Skip to content

Commit 06ffb15

Browse files
inotia00anddea
authored andcommitted
fix(YouTube - Spoof client): Restore playback speed menu when spoofing to an iOS, Android TV, Android Testsuite client
1 parent 99af80b commit 06ffb15

10 files changed

Lines changed: 247 additions & 93 deletions

File tree

src/main/kotlin/app/revanced/patches/shared/litho/LithoFilterPatch.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import app.revanced.patches.shared.litho.fingerprints.EmptyComponentsFingerprint
1414
import app.revanced.patches.shared.litho.fingerprints.LithoFilterPatchConstructorFingerprint
1515
import app.revanced.patches.shared.litho.fingerprints.PathBuilderFingerprint
1616
import app.revanced.patches.shared.litho.fingerprints.SetByteBufferFingerprint
17-
import app.revanced.util.getEmptyStringInstructionIndex
17+
import app.revanced.util.getStringInstructionIndex
1818
import app.revanced.util.getTargetIndex
1919
import app.revanced.util.getTargetIndexReversed
2020
import app.revanced.util.getTargetIndexWithFieldReferenceType
@@ -140,7 +140,7 @@ object LithoFilterPatch : BytecodePatch(
140140
val stringBuilderRegister =
141141
getInstruction<TwoRegisterInstruction>(stringBuilderIndex).registerA
142142

143-
val emptyStringIndex = getEmptyStringInstructionIndex()
143+
val emptyStringIndex = getStringInstructionIndex("")
144144

145145
val identifierIndex = getTargetIndexReversed(emptyStringIndex, Opcode.IPUT_OBJECT)
146146
val identifierRegister =

src/main/kotlin/app/revanced/patches/shared/spoofuseragent/BaseSpoofUserAgentPatch.kt

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import app.revanced.patches.shared.transformation.IMethodCall
88
import app.revanced.patches.shared.transformation.Instruction35cInfo
99
import app.revanced.patches.shared.transformation.filterMapInstruction35c
1010
import app.revanced.util.getReference
11+
import app.revanced.util.indexOfFirstInstruction
1112
import com.android.tools.smali.dexlib2.Opcode
12-
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
1313
import com.android.tools.smali.dexlib2.iface.ClassDef
1414
import com.android.tools.smali.dexlib2.iface.Method
1515
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
1616
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
1717
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
18+
import com.android.tools.smali.dexlib2.iface.reference.StringReference
1819

1920
abstract class BaseSpoofUserAgentPatch(
2021
private val packageName: String
@@ -36,45 +37,39 @@ abstract class BaseSpoofUserAgentPatch(
3637

3738
// Replace the result of context.getPackageName(), if it is used in a user agent string.
3839
mutableMethod.apply {
39-
var isTargetMethod = true
40+
// After context.getPackageName() the result is moved to a register.
41+
val targetRegister = (
42+
getInstruction(instructionIndex + 1)
43+
as? OneRegisterInstruction ?: return
44+
).registerA
4045

41-
for ((index, instruction) in implementation!!.instructions.withIndex()) {
42-
if (instruction.opcode != Opcode.CONST_STRING)
43-
continue
46+
// IndexOutOfBoundsException is possible here,
47+
// but no such occurrences are present in the app.
48+
val referee =
49+
getInstruction(instructionIndex + 2).getReference<MethodReference>()?.toString()
4450

45-
val constString = getInstruction<BuilderInstruction21c>(index).reference.toString()
46-
47-
if (constString != "android.resource://" && constString != "gcore_")
48-
continue
49-
50-
isTargetMethod = false
51-
break
51+
// Only replace string builder usage.
52+
if (referee != USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE) {
53+
return
5254
}
5355

54-
if (isTargetMethod) {
55-
// After context.getPackageName() the result is moved to a register.
56-
val targetRegister = (
57-
getInstruction(instructionIndex + 1)
58-
as? OneRegisterInstruction ?: return
59-
).registerA
60-
61-
// IndexOutOfBoundsException is not possible here,
62-
// but no such occurrences are present in the app.
63-
val referee =
64-
getInstruction(instructionIndex + 2).getReference<MethodReference>()?.toString()
65-
66-
// This can technically also match non-user agent string builder append methods,
67-
// but no such occurrences are present in the app.
68-
if (referee != "Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;") {
69-
return
70-
}
71-
72-
// Overwrite the result of context.getPackageName() with the original package name.
73-
replaceInstruction(
74-
instructionIndex + 1,
75-
"const-string v$targetRegister, \"$packageName\"",
76-
)
56+
// Do not change the package name in methods that use resources, or for methods that use GmsCore.
57+
// Changing these package names will result in playback limitations,
58+
// particularly Android VR background audio only playback.
59+
val resourceOrGmsStringInstructionIndex = indexOfFirstInstruction {
60+
val reference = getReference<StringReference>()
61+
opcode == Opcode.CONST_STRING &&
62+
(reference?.string == "android.resource://" || reference?.string == "gcore_")
7763
}
64+
if (resourceOrGmsStringInstructionIndex >= 0) {
65+
return
66+
}
67+
68+
// Overwrite the result of context.getPackageName() with the original package name.
69+
replaceInstruction(
70+
instructionIndex + 1,
71+
"const-string v$targetRegister, \"$packageName\"",
72+
)
7873
}
7974
}
8075

@@ -92,4 +87,9 @@ abstract class BaseSpoofUserAgentPatch(
9287
"Ljava/lang/String;",
9388
),
9489
}
90+
91+
private companion object {
92+
private const val USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE =
93+
"Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;"
94+
}
9595
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package app.revanced.patches.youtube.utils.fingerprints
2+
3+
import app.revanced.patcher.extensions.or
4+
import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.VarispeedUnavailableTitle
5+
import app.revanced.util.fingerprint.LiteralValueFingerprint
6+
import com.android.tools.smali.dexlib2.AccessFlags
7+
import com.android.tools.smali.dexlib2.Opcode
8+
9+
internal object PlaybackRateBottomSheetBuilderFingerprint : LiteralValueFingerprint(
10+
returnType = "V",
11+
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
12+
parameters = emptyList(),
13+
opcodes = listOf(
14+
Opcode.IGET_BOOLEAN,
15+
Opcode.IF_EQZ,
16+
),
17+
literalSupplier = { VarispeedUnavailableTitle }
18+
)

src/main/kotlin/app/revanced/patches/youtube/utils/fix/client/SpoofClientPatch.kt

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,23 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMu
1111
import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint
1212
import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint.indexOfModelInstruction
1313
import app.revanced.patches.youtube.utils.compatibility.Constants
14+
import app.revanced.patches.youtube.utils.fingerprints.PlaybackRateBottomSheetBuilderFingerprint
1415
import app.revanced.patches.youtube.utils.fix.client.fingerprints.BuildInitPlaybackRequestFingerprint
1516
import app.revanced.patches.youtube.utils.fix.client.fingerprints.BuildPlayerRequestURIFingerprint
17+
import app.revanced.patches.youtube.utils.fix.client.fingerprints.CreatePlaybackSpeedMenuItemFingerprint
1618
import app.revanced.patches.youtube.utils.fix.client.fingerprints.CreatePlayerRequestBodyFingerprint
1719
import app.revanced.patches.youtube.utils.fix.client.fingerprints.NerdsStatsVideoFormatBuilderFingerprint
1820
import app.revanced.patches.youtube.utils.fix.client.fingerprints.PlayerGestureConfigSyntheticFingerprint
1921
import app.revanced.patches.youtube.utils.fix.client.fingerprints.SetPlayerRequestClientTypeFingerprint
2022
import app.revanced.patches.youtube.utils.integrations.Constants.MISC_PATH
21-
import app.revanced.patches.youtube.utils.playertype.PlayerTypeHookPatch
23+
import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch
2224
import app.revanced.patches.youtube.utils.settings.SettingsPatch
2325
import app.revanced.patches.youtube.video.information.VideoInformationPatch
2426
import app.revanced.patches.youtube.video.playerresponse.PlayerResponseMethodHookPatch
2527
import app.revanced.util.getReference
2628
import app.revanced.util.getStringInstructionIndex
2729
import app.revanced.util.getWalkerMethod
30+
import app.revanced.util.indexOfFirstInstructionOrThrow
2831
import app.revanced.util.patch.BaseBytecodePatch
2932
import app.revanced.util.resultOrThrow
3033
import com.android.tools.smali.dexlib2.AccessFlags
@@ -42,11 +45,11 @@ object SpoofClientPatch : BaseBytecodePatch(
4245
name = "Spoof client",
4346
description = "Adds options to spoof the client to allow video playback.",
4447
dependencies = setOf(
45-
PlayerTypeHookPatch::class,
4648
PlayerResponseMethodHookPatch::class,
4749
SettingsPatch::class,
4850
VideoInformationPatch::class,
4951
SpoofUserAgentPatch::class,
52+
SharedResourceIdPatch::class,
5053
),
5154
compatiblePackages = Constants.COMPATIBLE_PACKAGE,
5255
fingerprints = setOf(
@@ -60,6 +63,10 @@ object SpoofClientPatch : BaseBytecodePatch(
6063
// Player gesture config.
6164
PlayerGestureConfigSyntheticFingerprint,
6265

66+
// Player speed menu item.
67+
CreatePlaybackSpeedMenuItemFingerprint,
68+
PlaybackRateBottomSheetBuilderFingerprint,
69+
6370
// Nerds stats video format.
6471
NerdsStatsVideoFormatBuilderFingerprint,
6572
)
@@ -257,26 +264,74 @@ object SpoofClientPatch : BaseBytecodePatch(
257264
// region fix player gesture.
258265

259266
PlayerGestureConfigSyntheticFingerprint.resultOrThrow().let {
260-
arrayOf(3, 9).forEach { offSet ->
261-
it.getWalkerMethod(context, it.scanResult.patternScanResult!!.endIndex - offSet)
262-
.apply {
263-
val index = implementation!!.instructions.size - 1
264-
val register = getInstruction<OneRegisterInstruction>(index).registerA
267+
val endIndex = it.scanResult.patternScanResult!!.endIndex
268+
val downAndOutLandscapeAllowedIndex = endIndex - 3
269+
val downAndOutPortraitAllowedIndex = endIndex - 9
265270

266-
addInstructions(
267-
index,
268-
"""
269-
invoke-static {v$register}, $INTEGRATIONS_CLASS_DESCRIPTOR->enablePlayerGesture(Z)Z
270-
move-result v$register
271+
arrayOf(
272+
downAndOutLandscapeAllowedIndex,
273+
downAndOutPortraitAllowedIndex,
274+
).forEach { index ->
275+
val gestureAllowedMethod = it.getWalkerMethod(context, index)
276+
277+
gestureAllowedMethod.apply {
278+
val isAllowedIndex = getInstructions().lastIndex
279+
val isAllowed = getInstruction<OneRegisterInstruction>(isAllowedIndex).registerA
280+
281+
addInstructions(
282+
isAllowedIndex,
271283
"""
272-
)
273-
}
284+
invoke-static { v$isAllowed }, $INTEGRATIONS_CLASS_DESCRIPTOR->enablePlayerGesture(Z)Z
285+
move-result v$isAllowed
286+
""",
287+
)
288+
}
289+
}
290+
}
291+
292+
// endregion
293+
294+
// region fix playback speed menu item.
295+
296+
// fix for iOS, Android Testsuite
297+
CreatePlaybackSpeedMenuItemFingerprint.resultOrThrow().let {
298+
val scanResult = it.scanResult.patternScanResult!!
299+
if (scanResult.startIndex != 0) throw PatchException("Unexpected start index: ${scanResult.startIndex}")
300+
301+
it.mutableMethod.apply {
302+
// Find the conditional check if the playback speed menu item is not created.
303+
val shouldCreateMenuIndex = indexOfFirstInstructionOrThrow(scanResult.endIndex) { opcode == Opcode.IF_EQZ }
304+
val shouldCreateMenuRegister = getInstruction<OneRegisterInstruction>(shouldCreateMenuIndex).registerA
305+
306+
addInstructions(
307+
shouldCreateMenuIndex,
308+
"""
309+
invoke-static { v$shouldCreateMenuRegister }, $INTEGRATIONS_CLASS_DESCRIPTOR->forceCreatePlaybackSpeedMenu(Z)Z
310+
move-result v$shouldCreateMenuRegister
311+
""",
312+
)
313+
}
314+
}
315+
316+
// fix for Android TV
317+
PlaybackRateBottomSheetBuilderFingerprint.resultOrThrow().let {
318+
it.mutableMethod.apply {
319+
val targetIndex = it.scanResult.patternScanResult!!.endIndex
320+
val targetRegister = getInstruction<OneRegisterInstruction>(targetIndex).registerA
321+
322+
addInstructions(
323+
targetIndex,
324+
"""
325+
invoke-static { v$targetRegister }, $INTEGRATIONS_CLASS_DESCRIPTOR->forceCreatePlaybackSpeedMenuReversed(Z)Z
326+
move-result v$targetRegister
327+
""",
328+
)
274329
}
275330
}
276331

277332
// endregion
278333

279-
// region append spoof info
334+
// region append spoof info.
280335

281336
NerdsStatsVideoFormatBuilderFingerprint.resultOrThrow().mutableMethod.apply {
282337
for (index in implementation!!.instructions.size - 1 downTo 0) {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package app.revanced.patches.youtube.utils.fix.client.fingerprints
2+
3+
import app.revanced.patcher.extensions.or
4+
import app.revanced.patcher.fingerprint.MethodFingerprint
5+
import com.android.tools.smali.dexlib2.AccessFlags
6+
import com.android.tools.smali.dexlib2.Opcode
7+
8+
internal object CreatePlaybackSpeedMenuItemFingerprint : MethodFingerprint(
9+
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
10+
returnType = "V",
11+
opcodes = listOf(
12+
Opcode.IGET_OBJECT, // First instruction of the method
13+
Opcode.IGET_OBJECT,
14+
Opcode.IGET_OBJECT,
15+
Opcode.CONST_4,
16+
Opcode.IF_EQZ,
17+
Opcode.INVOKE_INTERFACE,
18+
null // MOVE_RESULT or MOVE_RESULT_OBJECT, Return value controls the creation of the playback speed menu item.
19+
),
20+
// 19.01 and earlier is missing the second parameter.
21+
// Since this fingerprint is somewhat weak, work around by checking for both method parameter signatures.
22+
customFingerprint = custom@{ methodDef, _ ->
23+
// 19.01 and earlier parameters are: "[L"
24+
// 19.02+ parameters are "[L", "F"
25+
val parameterTypes = methodDef.parameterTypes
26+
val firstParameter = parameterTypes.firstOrNull()
27+
28+
if (firstParameter == null || !firstParameter.startsWith("[L")) {
29+
return@custom false
30+
}
31+
32+
parameterTypes.size == 1 || (parameterTypes.size == 2 && parameterTypes[1] == "F")
33+
},
34+
)
Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package app.revanced.patches.youtube.utils.fix.client.fingerprints
22

33
import app.revanced.patcher.extensions.or
4-
import app.revanced.patcher.fingerprint.MethodFingerprint
54
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
6-
import app.revanced.patches.youtube.utils.fix.client.fingerprints.PlayerGestureConfigSyntheticFingerprint.indexOfDownAndOutAllowedInstruction
5+
import app.revanced.patcher.fingerprint.MethodFingerprint
76
import app.revanced.util.getReference
87
import app.revanced.util.indexOfFirstInstruction
98
import com.android.tools.smali.dexlib2.AccessFlags
@@ -12,8 +11,7 @@ import com.android.tools.smali.dexlib2.iface.Method
1211
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
1312

1413
/**
15-
* Annotation [FuzzyPatternScanMethod] is required to maintain compatibility with YouTube v18.29.38 ~ v18.32.39.
16-
*
14+
* [FuzzyPatternScanMethod] was added to maintain compatibility for YouTube v18.29.38 to v18.32.39.
1715
* TODO: Remove this annotation if support for YouTube v18.29.38 to v18.32.39 is dropped.
1816
*/
1917
@FuzzyPatternScanMethod(5)
@@ -30,28 +28,28 @@ internal object PlayerGestureConfigSyntheticFingerprint : MethodFingerprint(
3028
Opcode.IGET_OBJECT,
3129
Opcode.INVOKE_INTERFACE,
3230
Opcode.MOVE_RESULT_OBJECT,
33-
Opcode.INVOKE_VIRTUAL, // playerGestureConfig.downAndOutLandscapeAllowed
31+
Opcode.INVOKE_VIRTUAL, // playerGestureConfig.downAndOutLandscapeAllowed.
3432
Opcode.MOVE_RESULT,
3533
Opcode.CHECK_CAST,
3634
Opcode.IPUT_BOOLEAN,
3735
Opcode.INVOKE_INTERFACE,
3836
Opcode.MOVE_RESULT_OBJECT,
39-
Opcode.INVOKE_VIRTUAL, // playerGestureConfig.downAndOutPortraitAllowed
37+
Opcode.INVOKE_VIRTUAL, // playerGestureConfig.downAndOutPortraitAllowed.
4038
Opcode.MOVE_RESULT,
4139
Opcode.IPUT_BOOLEAN,
4240
Opcode.RETURN_VOID,
4341
),
4442
customFingerprint = { methodDef, classDef ->
45-
// This method is always called "a" because this kind of class always has a single method.
46-
methodDef.name == "a" && classDef.methods.count() == 2 &&
47-
indexOfDownAndOutAllowedInstruction(methodDef) >= 0
48-
}
49-
) {
50-
fun indexOfDownAndOutAllowedInstruction(methodDef: Method) =
51-
methodDef.indexOfFirstInstruction {
52-
val reference = getReference<MethodReference>()
53-
reference?.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/media/PlayerConfigModel;" &&
43+
fun indexOfDownAndOutAllowedInstruction(methodDef: Method) =
44+
methodDef.indexOfFirstInstruction {
45+
val reference = getReference<MethodReference>()
46+
reference?.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/media/PlayerConfigModel;" &&
5447
reference.parameterTypes.isEmpty() &&
5548
reference.returnType == "Z"
56-
}
57-
}
49+
}
50+
51+
// This method is always called "a" because this kind of class always has a single method.
52+
methodDef.name == "a" && classDef.methods.count() == 2 &&
53+
indexOfDownAndOutAllowedInstruction(methodDef) >= 0
54+
},
55+
)

0 commit comments

Comments
 (0)