11package io.github.chsbuffer.revancedxposed.youtube.video
22
33import app.revanced.extension.youtube.patches.VideoInformation
4+ import com.google.android.libraries.youtube.innertube.model.media.VideoQuality
45import de.robv.android.xposed.XC_MethodHook
6+ import de.robv.android.xposed.XposedBridge
7+ import io.github.chsbuffer.revancedxposed.AccessFlags
58import io.github.chsbuffer.revancedxposed.Opcode
9+ import io.github.chsbuffer.revancedxposed.findFirstFieldByExactType
10+ import io.github.chsbuffer.revancedxposed.fingerprint
611import io.github.chsbuffer.revancedxposed.getStaticObjectField
712import io.github.chsbuffer.revancedxposed.opcodes
813import io.github.chsbuffer.revancedxposed.strings
914import io.github.chsbuffer.revancedxposed.youtube.YoutubeHook
1015import org.luckypray.dexkit.query.enums.OpCodeMatchType
1116import org.luckypray.dexkit.result.FieldUsingType
17+ import org.luckypray.dexkit.wrap.DexClass
1218import java.lang.reflect.Method
1319import java.lang.reflect.Modifier
1420
@@ -30,15 +36,21 @@ class PlaybackController(
3036 private val seekToRelative : Method ,
3137 val seekSourceNone : Any
3238) : VideoInformation.PlaybackController {
33- override fun seekTo (videoTime : Long ): Boolean {
39+ override fun patch_seekTo (videoTime : Long ): Boolean {
3440 return seekTo.invoke(obj, videoTime, seekSourceNone) as Boolean
3541 }
3642
37- override fun seekToRelative (videoTimeOffset : Long ) {
43+ override fun patch_seekToRelative (videoTimeOffset : Long ) {
3844 seekToRelative.invoke(obj)
3945 }
4046}
4147
48+ private lateinit var getQualityName: (VideoQuality ) -> String
49+ private lateinit var getResolution: (VideoQuality ) -> Int
50+
51+ fun VideoQuality.getResolution () = getResolution(this )
52+ fun VideoQuality.getQualityName () = getQualityName(this )
53+
4254fun YoutubeHook.VideoInformationHook () {
4355 dependsOn(
4456 ::VideoIdPatch ,
@@ -245,4 +257,92 @@ fun YoutubeHook.VideoInformationHook() {
245257 // TODO Hook the user playback speed selection.
246258
247259 // TODO Handle new playback speed menu.
260+
261+ // videoQuality
262+ val YOUTUBE_VIDEO_QUALITY_CLASS_TYPE =
263+ " Lcom/google/android/libraries/youtube/innertube/model/media/VideoQuality;"
264+
265+ val videoQualityClass = DexClass (YOUTUBE_VIDEO_QUALITY_CLASS_TYPE ).toClass()
266+ val qualityNameField = videoQualityClass.findFirstFieldByExactType(String ::class .java)
267+ val resolutionField = videoQualityClass.findFirstFieldByExactType(Int ::class .java)
268+
269+ getQualityName = { quality -> qualityNameField.get(quality) as String }
270+ getResolution = { quality -> resolutionField.get(quality) as Int }
271+
272+ // Fix bad data used by YouTube.
273+ XposedBridge .hookAllConstructors(
274+ videoQualityClass, object : XC_MethodHook () {
275+ override fun afterHookedMethod (param : MethodHookParam ) {
276+ val quality = param.thisObject as VideoQuality
277+ val newResolution = VideoInformation .fixVideoQualityResolution(
278+ quality.getQualityName(), quality.getResolution()
279+ )
280+ resolutionField.set(quality, newResolution)
281+ }
282+ })
283+
284+ val videoQualitySetterFingerprint = getDexMethod(" videoQualitySetterFingerprint" ) {
285+ fingerprint {
286+ accessFlags(AccessFlags .PUBLIC , AccessFlags .FINAL )
287+ returns(" V" )
288+ parameters(" [L" , " I" , " Z" )
289+ opcodes(
290+ Opcode .IF_EQZ ,
291+ Opcode .INVOKE_VIRTUAL ,
292+ Opcode .MOVE_RESULT_OBJECT ,
293+ Opcode .INVOKE_VIRTUAL ,
294+ Opcode .IPUT_BOOLEAN ,
295+ )
296+ strings(" menu_item_video_quality" )
297+ }
298+ }
299+
300+ getDexMethod(" setVideoQualityFingerprint" ) {
301+ fingerprint {
302+ returns(" V" )
303+ parameters(" L" )
304+ opcodes(
305+ Opcode .IGET_OBJECT ,
306+ Opcode .IPUT_OBJECT ,
307+ Opcode .IGET_OBJECT ,
308+ )
309+ classMatcher {
310+ className = videoQualitySetterFingerprint.className
311+ }
312+ }.also { method ->
313+ val usingFields = method.usingFields
314+ getDexField(" onItemClickListenerClassReference" ) {
315+ usingFields[0 ].field
316+ }
317+ getDexField(" setQualityFieldReference" ) {
318+ usingFields[1 ].field
319+ }
320+ getDexMethod(" setQualityMenuIndexMethod" ) {
321+ usingFields[1 ].field.type.findMethod {
322+ matcher { addParamType { descriptor = YOUTUBE_VIDEO_QUALITY_CLASS_TYPE } }
323+ }.single()
324+ }
325+ }
326+ }
327+
328+ // Detect video quality changes and override the current quality.
329+ videoQualitySetterFingerprint.hookMethod(object : XC_MethodHook () {
330+ val onItemClickListenerClass = getDexField(" onItemClickListenerClassReference" ).toField()
331+ val setQualityField = getDexField(" setQualityFieldReference" ).toField()
332+ val setQualityMenuIndexMethod = getDexMethod(" setQualityMenuIndexMethod" ).toMethod()
333+
334+ @Suppress(" UNCHECKED_CAST" )
335+ override fun beforeHookedMethod (param : MethodHookParam ) {
336+ val qualities = param.args[0 ] as Array <out VideoQuality >
337+ val originalQualityIndex = param.args[1 ] as Int
338+ val menu = param.thisObject.let { onItemClickListenerClass.get(it) }
339+ .let { setQualityField.get(it) }
340+
341+ param.args[1 ] = VideoInformation .setVideoQuality(
342+ qualities,
343+ { quality -> setQualityMenuIndexMethod(menu, quality) },
344+ originalQualityIndex
345+ )
346+ }
347+ })
248348}
0 commit comments