Skip to content

Commit 8042cab

Browse files
committed
YouTube: Add Remember Video Quality
1 parent fff50a9 commit 8042cab

File tree

4 files changed

+199
-24
lines changed

4 files changed

+199
-24
lines changed

app/src/main/java/io/github/chsbuffer/revancedxposed/BaseHook.kt

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,15 @@ interface IHook {
2727
fun Hook()
2828

2929
fun DexMethod.hookMethod(callback: XC_MethodHook) {
30-
when {
31-
isMethod -> XposedBridge.hookMethod(getMethodInstance(classLoader), callback)
32-
isConstructor -> XposedBridge.hookMethod(
33-
getConstructorInstance(classLoader),
34-
callback
35-
)
36-
37-
else -> throw NotImplementedError()
38-
}
39-
}
30+
XposedBridge.hookMethod(toMember(), callback) }
4031

4132
fun DexClass.toClass() = getInstance(classLoader)
4233
fun DexMethod.toMethod() = getMethodInstance(classLoader)
34+
fun DexMethod.toMember() = when {
35+
isMethod -> getMethodInstance(classLoader)
36+
isConstructor -> getConstructorInstance(classLoader)
37+
else -> throw NotImplementedError()
38+
}
4339
fun DexField.toField() = getFieldInstance(classLoader)
4440
}
4541

app/src/main/java/io/github/chsbuffer/revancedxposed/FingerprintCompat.kt

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import org.luckypray.dexkit.result.MethodData
88
import org.luckypray.dexkit.util.DexSignUtil.getTypeName
99
import java.lang.reflect.Modifier
1010

11+
private fun getTypeNameCompat(it: String): String? {
12+
return if (it.trimStart('[').startsWith('L') && !it.endsWith(';')) null
13+
else getTypeName(it)
14+
}
15+
1116
class Fingerprint(val dexkit: DexKitBridge, init: Fingerprint.() -> Unit) {
1217
var classMatcher: ClassMatcher? = null
1318
val methodMatcher = MethodMatcher()
@@ -35,14 +40,11 @@ class Fingerprint(val dexkit: DexKitBridge, init: Fingerprint.() -> Unit) {
3540
}
3641

3742
fun parameters(vararg parameters: String) {
38-
methodMatcher.paramTypes(parameters.map {
39-
if (it.trimStart('[').startsWith('L') && !it.endsWith(';')) null
40-
else getTypeName(it)
41-
})
43+
methodMatcher.paramTypes(parameters.map(::getTypeNameCompat))
4244
}
4345

4446
fun returns(returnType: String) {
45-
methodMatcher.returnType = getTypeName(returnType)
47+
getTypeNameCompat(returnType)?.let { methodMatcher.returnType = it }
4648
}
4749

4850
fun literal(literalSupplier: () -> Number) {
@@ -110,18 +112,11 @@ fun MethodMatcher.accessFlags(vararg accessFlags: AccessFlags) {
110112
}
111113

112114
fun MethodMatcher.parameters(vararg parameters: String) {
113-
this.paramTypes(parameters.map {
114-
if (it.trimStart('[').startsWith('L') && !it.endsWith(';')) null
115-
else getTypeName(it)
116-
})
115+
this.paramTypes(parameters.map(::getTypeNameCompat))
117116
}
118117

119118
fun MethodMatcher.returns(returnType: String) {
120-
this.returnType = getTypeName(returnType)
121-
}
122-
123-
fun MethodMatcher.definingClass(definingClass: String) {
124-
this.declaredClass = getTypeName(definingClass)
119+
getTypeNameCompat(returnType)?.let { this.returnType = it }
125120
}
126121

127122
fun MethodMatcher.literal(literalSupplier: () -> Number) {

app/src/main/java/io/github/chsbuffer/revancedxposed/youtube/YoutubeHook.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import io.github.chsbuffer.revancedxposed.youtube.misc.BackgroundPlayback
1616
import io.github.chsbuffer.revancedxposed.youtube.misc.LithoFilter
1717
import io.github.chsbuffer.revancedxposed.youtube.misc.RemoveTrackingQueryParameter
1818
import io.github.chsbuffer.revancedxposed.youtube.misc.SettingsHook
19+
import io.github.chsbuffer.revancedxposed.youtube.video.RememberVideoQuality
1920

2021
class YoutubeHook(
2122
app: Application,
@@ -32,6 +33,7 @@ class YoutubeHook(
3233
::SponsorBlock,
3334
::NavigationButtons,
3435
::SwipeControls,
36+
::RememberVideoQuality,
3537
// make sure settingsHook at end to build preferences
3638
::SettingsHook
3739
)
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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

Comments
 (0)