fix(YouTube/Spoof client): restore playback speed menu when spoofing to an iOS, Android TV, Android Testsuite client 95f290f113

This commit is contained in:
inotia00 2024-06-11 01:51:16 +09:00
parent 5208e50817
commit b9ef6ea3bf
11 changed files with 250 additions and 97 deletions

View File

@ -14,7 +14,7 @@ import app.revanced.patches.shared.litho.fingerprints.EmptyComponentsFingerprint
import app.revanced.patches.shared.litho.fingerprints.LithoFilterPatchConstructorFingerprint import app.revanced.patches.shared.litho.fingerprints.LithoFilterPatchConstructorFingerprint
import app.revanced.patches.shared.litho.fingerprints.PathBuilderFingerprint import app.revanced.patches.shared.litho.fingerprints.PathBuilderFingerprint
import app.revanced.patches.shared.litho.fingerprints.SetByteBufferFingerprint import app.revanced.patches.shared.litho.fingerprints.SetByteBufferFingerprint
import app.revanced.util.getEmptyStringInstructionIndex import app.revanced.util.getStringInstructionIndex
import app.revanced.util.getTargetIndex import app.revanced.util.getTargetIndex
import app.revanced.util.getTargetIndexReversed import app.revanced.util.getTargetIndexReversed
import app.revanced.util.getTargetIndexWithFieldReferenceType import app.revanced.util.getTargetIndexWithFieldReferenceType
@ -140,7 +140,7 @@ object LithoFilterPatch : BytecodePatch(
val stringBuilderRegister = val stringBuilderRegister =
getInstruction<TwoRegisterInstruction>(stringBuilderIndex).registerA getInstruction<TwoRegisterInstruction>(stringBuilderIndex).registerA
val emptyStringIndex = getEmptyStringInstructionIndex() val emptyStringIndex = getStringInstructionIndex("")
val identifierIndex = getTargetIndexReversed(emptyStringIndex, Opcode.IPUT_OBJECT) val identifierIndex = getTargetIndexReversed(emptyStringIndex, Opcode.IPUT_OBJECT)
val identifierRegister = val identifierRegister =

View File

@ -8,13 +8,14 @@ import app.revanced.patches.shared.transformation.IMethodCall
import app.revanced.patches.shared.transformation.Instruction35cInfo import app.revanced.patches.shared.transformation.Instruction35cInfo
import app.revanced.patches.shared.transformation.filterMapInstruction35c import app.revanced.patches.shared.transformation.filterMapInstruction35c
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
import com.android.tools.smali.dexlib2.iface.ClassDef import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.Instruction import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.StringReference
abstract class BaseSpoofUserAgentPatch( abstract class BaseSpoofUserAgentPatch(
private val packageName: String private val packageName: String
@ -36,45 +37,39 @@ abstract class BaseSpoofUserAgentPatch(
// Replace the result of context.getPackageName(), if it is used in a user agent string. // Replace the result of context.getPackageName(), if it is used in a user agent string.
mutableMethod.apply { mutableMethod.apply {
var isTargetMethod = true // After context.getPackageName() the result is moved to a register.
val targetRegister = (
getInstruction(instructionIndex + 1)
as? OneRegisterInstruction ?: return
).registerA
for ((index, instruction) in implementation!!.instructions.withIndex()) { // IndexOutOfBoundsException is possible here,
if (instruction.opcode != Opcode.CONST_STRING) // but no such occurrences are present in the app.
continue val referee =
getInstruction(instructionIndex + 2).getReference<MethodReference>()?.toString()
val constString = getInstruction<BuilderInstruction21c>(index).reference.toString() // Only replace string builder usage.
if (referee != USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE) {
if (constString != "android.resource://" && constString != "gcore_") return
continue
isTargetMethod = false
break
} }
if (isTargetMethod) { // Do not change the package name in methods that use resources, or for methods that use GmsCore.
// After context.getPackageName() the result is moved to a register. // Changing these package names will result in playback limitations,
val targetRegister = ( // particularly Android VR background audio only playback.
getInstruction(instructionIndex + 1) val resourceOrGmsStringInstructionIndex = indexOfFirstInstruction {
as? OneRegisterInstruction ?: return val reference = getReference<StringReference>()
).registerA opcode == Opcode.CONST_STRING &&
(reference?.string == "android.resource://" || reference?.string == "gcore_")
// IndexOutOfBoundsException is not possible here,
// but no such occurrences are present in the app.
val referee =
getInstruction(instructionIndex + 2).getReference<MethodReference>()?.toString()
// This can technically also match non-user agent string builder append methods,
// but no such occurrences are present in the app.
if (referee != "Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;") {
return
}
// Overwrite the result of context.getPackageName() with the original package name.
replaceInstruction(
instructionIndex + 1,
"const-string v$targetRegister, \"$packageName\"",
)
} }
if (resourceOrGmsStringInstructionIndex >= 0) {
return
}
// Overwrite the result of context.getPackageName() with the original package name.
replaceInstruction(
instructionIndex + 1,
"const-string v$targetRegister, \"$packageName\"",
)
} }
} }
@ -92,4 +87,9 @@ abstract class BaseSpoofUserAgentPatch(
"Ljava/lang/String;", "Ljava/lang/String;",
), ),
} }
private companion object {
private const val USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE =
"Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;"
}
} }

View File

@ -0,0 +1,18 @@
package app.revanced.patches.youtube.utils.fingerprints
import app.revanced.patcher.extensions.or
import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.VarispeedUnavailableTitle
import app.revanced.util.fingerprint.LiteralValueFingerprint
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
internal object PlaybackRateBottomSheetBuilderFingerprint : LiteralValueFingerprint(
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = emptyList(),
opcodes = listOf(
Opcode.IGET_BOOLEAN,
Opcode.IF_EQZ,
),
literalSupplier = { VarispeedUnavailableTitle }
)

View File

@ -11,20 +11,23 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMu
import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint
import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint.indexOfModelInstruction import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint.indexOfModelInstruction
import app.revanced.patches.youtube.utils.compatibility.Constants import app.revanced.patches.youtube.utils.compatibility.Constants
import app.revanced.patches.youtube.utils.fingerprints.PlaybackRateBottomSheetBuilderFingerprint
import app.revanced.patches.youtube.utils.fix.client.fingerprints.BuildInitPlaybackRequestFingerprint import app.revanced.patches.youtube.utils.fix.client.fingerprints.BuildInitPlaybackRequestFingerprint
import app.revanced.patches.youtube.utils.fix.client.fingerprints.BuildPlayerRequestURIFingerprint import app.revanced.patches.youtube.utils.fix.client.fingerprints.BuildPlayerRequestURIFingerprint
import app.revanced.patches.youtube.utils.fix.client.fingerprints.CreatePlaybackSpeedMenuItemFingerprint
import app.revanced.patches.youtube.utils.fix.client.fingerprints.CreatePlayerRequestBodyFingerprint import app.revanced.patches.youtube.utils.fix.client.fingerprints.CreatePlayerRequestBodyFingerprint
import app.revanced.patches.youtube.utils.fix.client.fingerprints.NerdsStatsVideoFormatBuilderFingerprint import app.revanced.patches.youtube.utils.fix.client.fingerprints.NerdsStatsVideoFormatBuilderFingerprint
import app.revanced.patches.youtube.utils.fix.client.fingerprints.PlayerGestureConfigSyntheticFingerprint import app.revanced.patches.youtube.utils.fix.client.fingerprints.PlayerGestureConfigSyntheticFingerprint
import app.revanced.patches.youtube.utils.fix.client.fingerprints.SetPlayerRequestClientTypeFingerprint import app.revanced.patches.youtube.utils.fix.client.fingerprints.SetPlayerRequestClientTypeFingerprint
import app.revanced.patches.youtube.utils.integrations.Constants.MISC_PATH import app.revanced.patches.youtube.utils.integrations.Constants.MISC_PATH
import app.revanced.patches.youtube.utils.playertype.PlayerTypeHookPatch import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch
import app.revanced.patches.youtube.utils.settings.SettingsPatch import app.revanced.patches.youtube.utils.settings.SettingsPatch
import app.revanced.patches.youtube.video.information.VideoInformationPatch import app.revanced.patches.youtube.video.information.VideoInformationPatch
import app.revanced.patches.youtube.video.playerresponse.PlayerResponseMethodHookPatch import app.revanced.patches.youtube.video.playerresponse.PlayerResponseMethodHookPatch
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.getStringInstructionIndex import app.revanced.util.getStringInstructionIndex
import app.revanced.util.getWalkerMethod import app.revanced.util.getWalkerMethod
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.patch.BaseBytecodePatch import app.revanced.util.patch.BaseBytecodePatch
import app.revanced.util.resultOrThrow import app.revanced.util.resultOrThrow
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
@ -42,11 +45,11 @@ object SpoofClientPatch : BaseBytecodePatch(
name = "Spoof client", name = "Spoof client",
description = "Adds options to spoof the client to allow video playback.", description = "Adds options to spoof the client to allow video playback.",
dependencies = setOf( dependencies = setOf(
PlayerTypeHookPatch::class,
PlayerResponseMethodHookPatch::class, PlayerResponseMethodHookPatch::class,
SettingsPatch::class, SettingsPatch::class,
VideoInformationPatch::class, VideoInformationPatch::class,
SpoofUserAgentPatch::class, SpoofUserAgentPatch::class,
SharedResourceIdPatch::class,
), ),
compatiblePackages = Constants.COMPATIBLE_PACKAGE, compatiblePackages = Constants.COMPATIBLE_PACKAGE,
fingerprints = setOf( fingerprints = setOf(
@ -60,6 +63,10 @@ object SpoofClientPatch : BaseBytecodePatch(
// Player gesture config. // Player gesture config.
PlayerGestureConfigSyntheticFingerprint, PlayerGestureConfigSyntheticFingerprint,
// Player speed menu item.
CreatePlaybackSpeedMenuItemFingerprint,
PlaybackRateBottomSheetBuilderFingerprint,
// Nerds stats video format. // Nerds stats video format.
NerdsStatsVideoFormatBuilderFingerprint, NerdsStatsVideoFormatBuilderFingerprint,
) )
@ -257,26 +264,74 @@ object SpoofClientPatch : BaseBytecodePatch(
// region fix player gesture. // region fix player gesture.
PlayerGestureConfigSyntheticFingerprint.resultOrThrow().let { PlayerGestureConfigSyntheticFingerprint.resultOrThrow().let {
arrayOf(3, 9).forEach { offSet -> val endIndex = it.scanResult.patternScanResult!!.endIndex
it.getWalkerMethod(context, it.scanResult.patternScanResult!!.endIndex - offSet) val downAndOutLandscapeAllowedIndex = endIndex - 3
.apply { val downAndOutPortraitAllowedIndex = endIndex - 9
val index = implementation!!.instructions.size - 1
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstructions( arrayOf(
index, downAndOutLandscapeAllowedIndex,
""" downAndOutPortraitAllowedIndex,
invoke-static {v$register}, $INTEGRATIONS_CLASS_DESCRIPTOR->enablePlayerGesture(Z)Z ).forEach { index ->
move-result v$register val gestureAllowedMethod = it.getWalkerMethod(context, index)
gestureAllowedMethod.apply {
val isAllowedIndex = getInstructions().lastIndex
val isAllowed = getInstruction<OneRegisterInstruction>(isAllowedIndex).registerA
addInstructions(
isAllowedIndex,
""" """
) invoke-static { v$isAllowed }, $INTEGRATIONS_CLASS_DESCRIPTOR->enablePlayerGesture(Z)Z
} move-result v$isAllowed
""",
)
}
} }
} }
// endregion // endregion
// region append spoof info // region fix playback speed menu item.
// fix for iOS, Android Testsuite
CreatePlaybackSpeedMenuItemFingerprint.resultOrThrow().let {
val scanResult = it.scanResult.patternScanResult!!
if (scanResult.startIndex != 0) throw PatchException("Unexpected start index: ${scanResult.startIndex}")
it.mutableMethod.apply {
// Find the conditional check if the playback speed menu item is not created.
val shouldCreateMenuIndex = indexOfFirstInstructionOrThrow(scanResult.endIndex) { opcode == Opcode.IF_EQZ }
val shouldCreateMenuRegister = getInstruction<OneRegisterInstruction>(shouldCreateMenuIndex).registerA
addInstructions(
shouldCreateMenuIndex,
"""
invoke-static { v$shouldCreateMenuRegister }, $INTEGRATIONS_CLASS_DESCRIPTOR->forceCreatePlaybackSpeedMenu(Z)Z
move-result v$shouldCreateMenuRegister
""",
)
}
}
// fix for Android TV
PlaybackRateBottomSheetBuilderFingerprint.resultOrThrow().let {
it.mutableMethod.apply {
val targetIndex = it.scanResult.patternScanResult!!.endIndex
val targetRegister = getInstruction<OneRegisterInstruction>(targetIndex).registerA
addInstructions(
targetIndex,
"""
invoke-static { v$targetRegister }, $INTEGRATIONS_CLASS_DESCRIPTOR->forceCreatePlaybackSpeedMenuReversed(Z)Z
move-result v$targetRegister
""",
)
}
}
// endregion
// region append spoof info.
NerdsStatsVideoFormatBuilderFingerprint.resultOrThrow().mutableMethod.apply { NerdsStatsVideoFormatBuilderFingerprint.resultOrThrow().mutableMethod.apply {
for (index in implementation!!.instructions.size - 1 downTo 0) { for (index in implementation!!.instructions.size - 1 downTo 0) {

View File

@ -0,0 +1,34 @@
package app.revanced.patches.youtube.utils.fix.client.fingerprints
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
internal object CreatePlaybackSpeedMenuItemFingerprint : MethodFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
returnType = "V",
opcodes = listOf(
Opcode.IGET_OBJECT, // First instruction of the method
Opcode.IGET_OBJECT,
Opcode.IGET_OBJECT,
Opcode.CONST_4,
Opcode.IF_EQZ,
Opcode.INVOKE_INTERFACE,
null // MOVE_RESULT or MOVE_RESULT_OBJECT, Return value controls the creation of the playback speed menu item.
),
// 19.01 and earlier is missing the second parameter.
// Since this fingerprint is somewhat weak, work around by checking for both method parameter signatures.
customFingerprint = custom@{ methodDef, _ ->
// 19.01 and earlier parameters are: "[L"
// 19.02+ parameters are "[L", "F"
val parameterTypes = methodDef.parameterTypes
val firstParameter = parameterTypes.firstOrNull()
if (firstParameter == null || !firstParameter.startsWith("[L")) {
return@custom false
}
parameterTypes.size == 1 || (parameterTypes.size == 2 && parameterTypes[1] == "F")
},
)

View File

@ -1,9 +1,8 @@
package app.revanced.patches.youtube.utils.fix.client.fingerprints package app.revanced.patches.youtube.utils.fix.client.fingerprints
import app.revanced.patcher.extensions.or import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
import app.revanced.patches.youtube.utils.fix.client.fingerprints.PlayerGestureConfigSyntheticFingerprint.indexOfDownAndOutAllowedInstruction import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
@ -12,8 +11,7 @@ import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
/** /**
* Annotation [FuzzyPatternScanMethod] is required to maintain compatibility with YouTube v18.29.38 ~ v18.32.39. * [FuzzyPatternScanMethod] was added to maintain compatibility for YouTube v18.29.38 to v18.32.39.
*
* TODO: Remove this annotation if support for YouTube v18.29.38 to v18.32.39 is dropped. * TODO: Remove this annotation if support for YouTube v18.29.38 to v18.32.39 is dropped.
*/ */
@FuzzyPatternScanMethod(5) @FuzzyPatternScanMethod(5)
@ -30,28 +28,28 @@ internal object PlayerGestureConfigSyntheticFingerprint : MethodFingerprint(
Opcode.IGET_OBJECT, Opcode.IGET_OBJECT,
Opcode.INVOKE_INTERFACE, Opcode.INVOKE_INTERFACE,
Opcode.MOVE_RESULT_OBJECT, Opcode.MOVE_RESULT_OBJECT,
Opcode.INVOKE_VIRTUAL, // playerGestureConfig.downAndOutLandscapeAllowed Opcode.INVOKE_VIRTUAL, // playerGestureConfig.downAndOutLandscapeAllowed.
Opcode.MOVE_RESULT, Opcode.MOVE_RESULT,
Opcode.CHECK_CAST, Opcode.CHECK_CAST,
Opcode.IPUT_BOOLEAN, Opcode.IPUT_BOOLEAN,
Opcode.INVOKE_INTERFACE, Opcode.INVOKE_INTERFACE,
Opcode.MOVE_RESULT_OBJECT, Opcode.MOVE_RESULT_OBJECT,
Opcode.INVOKE_VIRTUAL, // playerGestureConfig.downAndOutPortraitAllowed Opcode.INVOKE_VIRTUAL, // playerGestureConfig.downAndOutPortraitAllowed.
Opcode.MOVE_RESULT, Opcode.MOVE_RESULT,
Opcode.IPUT_BOOLEAN, Opcode.IPUT_BOOLEAN,
Opcode.RETURN_VOID, Opcode.RETURN_VOID,
), ),
customFingerprint = { methodDef, classDef -> customFingerprint = { methodDef, classDef ->
// This method is always called "a" because this kind of class always has a single method. fun indexOfDownAndOutAllowedInstruction(methodDef: Method) =
methodDef.name == "a" && classDef.methods.count() == 2 && methodDef.indexOfFirstInstruction {
indexOfDownAndOutAllowedInstruction(methodDef) >= 0 val reference = getReference<MethodReference>()
} reference?.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/media/PlayerConfigModel;" &&
) {
fun indexOfDownAndOutAllowedInstruction(methodDef: Method) =
methodDef.indexOfFirstInstruction {
val reference = getReference<MethodReference>()
reference?.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/media/PlayerConfigModel;" &&
reference.parameterTypes.isEmpty() && reference.parameterTypes.isEmpty() &&
reference.returnType == "Z" reference.returnType == "Z"
} }
}
// This method is always called "a" because this kind of class always has a single method.
methodDef.name == "a" && classDef.methods.count() == 2 &&
indexOfDownAndOutAllowedInstruction(methodDef) >= 0
},
)

View File

@ -2,9 +2,8 @@ package app.revanced.patches.youtube.utils.flyoutmenu
import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.patch.BytecodePatch import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.annotation.Patch import app.revanced.patcher.patch.annotation.Patch
import app.revanced.patches.youtube.utils.flyoutmenu.fingerprints.PlaybackRateBottomSheetClassFingerprint import app.revanced.patches.youtube.utils.fingerprints.PlaybackRateBottomSheetBuilderFingerprint
import app.revanced.patches.youtube.utils.flyoutmenu.fingerprints.VideoQualityBottomSheetClassFingerprint import app.revanced.patches.youtube.utils.flyoutmenu.fingerprints.VideoQualityBottomSheetClassFingerprint
import app.revanced.patches.youtube.utils.integrations.Constants.INTEGRATIONS_PATH import app.revanced.patches.youtube.utils.integrations.Constants.INTEGRATIONS_PATH
import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch
@ -17,7 +16,7 @@ import app.revanced.util.resultOrThrow
) )
object FlyoutMenuHookPatch : BytecodePatch( object FlyoutMenuHookPatch : BytecodePatch(
setOf( setOf(
PlaybackRateBottomSheetClassFingerprint, PlaybackRateBottomSheetBuilderFingerprint,
VideoQualityBottomSheetClassFingerprint VideoQualityBottomSheetClassFingerprint
) )
) { ) {
@ -30,17 +29,12 @@ object FlyoutMenuHookPatch : BytecodePatch(
INTEGRATIONS_VIDEO_UTILS_CLASS_DESCRIPTOR INTEGRATIONS_VIDEO_UTILS_CLASS_DESCRIPTOR
)!!.mutableClass )!!.mutableClass
PlaybackRateBottomSheetClassFingerprint.resultOrThrow().let { PlaybackRateBottomSheetBuilderFingerprint.resultOrThrow().let {
it.mutableMethod.apply { it.mutableMethod.apply {
val playbackRateBottomSheetBuilderMethodName =
it.mutableClass.methods.find { method -> method.parameters.isEmpty() && method.returnType == "V" }
?.name
?: throw PatchException("Could not find PlaybackRateBottomSheetBuilderMethod")
val smaliInstructions = val smaliInstructions =
""" """
if-eqz v0, :ignore if-eqz v0, :ignore
invoke-virtual {v0}, $definingClass->$playbackRateBottomSheetBuilderMethodName()V invoke-virtual {v0}, $definingClass->$name()V
:ignore :ignore
return-void return-void
""" """

View File

@ -1,12 +0,0 @@
package app.revanced.patches.youtube.utils.flyoutmenu.fingerprints
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.AccessFlags
internal object PlaybackRateBottomSheetClassFingerprint : MethodFingerprint(
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = listOf("[L", "I"),
strings = listOf("menu_item_playback_speed")
)

View File

@ -95,6 +95,7 @@ object SharedResourceIdPatch : ResourcePatch() {
var TotalTime = -1L var TotalTime = -1L
var TouchArea = -1L var TouchArea = -1L
var VideoQualityBottomSheet = -1L var VideoQualityBottomSheet = -1L
var VarispeedUnavailableTitle = -1L
var VideoQualityUnavailableAnnouncement = -1L var VideoQualityUnavailableAnnouncement = -1L
var VoiceSearch = -1L var VoiceSearch = -1L
var YouTubeControlsOverlaySubtitleButton = -1L var YouTubeControlsOverlaySubtitleButton = -1L
@ -187,6 +188,7 @@ object SharedResourceIdPatch : ResourcePatch() {
TotalTime = getId(STRING, "total_time") TotalTime = getId(STRING, "total_time")
TouchArea = getId(ID, "touch_area") TouchArea = getId(ID, "touch_area")
VideoQualityBottomSheet = getId(LAYOUT, "video_quality_bottom_sheet_list_fragment_title") VideoQualityBottomSheet = getId(LAYOUT, "video_quality_bottom_sheet_list_fragment_title")
VarispeedUnavailableTitle = getId(STRING, "varispeed_unavailable_title")
VideoQualityUnavailableAnnouncement = VideoQualityUnavailableAnnouncement =
getId(STRING, "video_quality_unavailable_announcement") getId(STRING, "video_quality_unavailable_announcement")
VoiceSearch = getId(ID, "voice_search") VoiceSearch = getId(ID, "voice_search")

View File

@ -195,6 +195,73 @@ fun BytecodeContext.literalInstructionHook(
} }
} }
/**
* Get the index of the first instruction with the literal value or throw a [PatchException].
*
* @throws [PatchException] if the literal index not found.
*/
fun Method.indexOfWideLiteralInstructionOrThrow(literal: Long): Int {
val index = getWideLiteralInstructionIndex(literal)
if (index < 0) {
val value =
if (literal >= 2130706432) // 0x7f000000, general resource id
String.format("%#X", literal).lowercase()
else
literal.toString()
throw PatchException("Found literal value for: '$value' but method does not contain the id: $this")
}
return index
}
/**
* Get the index of the first [Instruction] that matches the predicate, starting from [startIndex].
*
* @param startIndex Optional starting index to start searching from.
* @return -1 if the instruction is not found.
* @see indexOfFirstInstructionOrThrow
*/
fun Method.indexOfFirstInstruction(startIndex: Int = 0, predicate: Instruction.() -> Boolean): Int {
val index = this.implementation!!.instructions.drop(startIndex).indexOfFirst(predicate)
return if (index >= 0) {
startIndex + index
} else {
-1
}
}
/**
* Get the index of the first [Instruction] that matches the predicate, starting from [startIndex].
*
* @return the index of the instruction
* @throws PatchException
* @see indexOfFirstInstruction
*/
fun Method.indexOfFirstInstructionOrThrow(startIndex: Int = 0, predicate: Instruction.() -> Boolean): Int {
val index = indexOfFirstInstruction(startIndex, predicate)
if (index < 0) {
throw PatchException("Could not find instruction index")
}
return index
}
/**
* @return The list of indices of the opcode in reverse order.
*/
fun Method.findOpcodeIndicesReversed(opcode: Opcode): List<Int> {
val indexes = implementation!!.instructions
.withIndex()
.filter { (_, instruction) -> instruction.opcode == opcode }
.map { (index, _) -> index }
.reversed()
if (indexes.isEmpty()) throw PatchException("No ${opcode.name} instructions found in: $this")
return indexes
}
/** /**
* Find the index of the first wide literal instruction with the given value. * Find the index of the first wide literal instruction with the given value.
* *
@ -206,8 +273,6 @@ fun Method.getWideLiteralInstructionIndex(literal: Long) = implementation?.let {
} }
} ?: -1 } ?: -1
fun Method.getEmptyStringInstructionIndex() = getStringInstructionIndex("")
fun Method.getStringInstructionIndex(value: String) = implementation?.let { fun Method.getStringInstructionIndex(value: String) = implementation?.let {
it.instructions.indexOfFirst { instruction -> it.instructions.indexOfFirst { instruction ->
instruction.opcode == Opcode.CONST_STRING instruction.opcode == Opcode.CONST_STRING

View File

@ -1496,8 +1496,7 @@ Limitation: Feed videos will play for less than 1 minute before encountering pla
Side effects include: Side effects include:
• No HDR video. • No HDR video.
• Playback speed menu is missing. • Higher video qualities may not be available.
• Higher video qualities may be missing.
• Watch history does not work with a brand account. • Watch history does not work with a brand account.
• Live streams cannot play as audio-only. • Live streams cannot play as audio-only.
• Live streams not available on Android 8.0."</string> • Live streams not available on Android 8.0."</string>
@ -1506,14 +1505,14 @@ Side effects include:
Side effects include: Side effects include:
• No HDR video. • No HDR video.
• Audio track menu and playback speed menu are missing. • Audio track menu is missing.
• Captions may not be available."</string> • Captions may not be available."</string>
<string name="revanced_spoof_client_use_android_tv_title">Android TV</string> <string name="revanced_spoof_client_use_android_tv_title">Android TV</string>
<string name="revanced_spoof_client_use_android_tv_summary">"Spoof client to Android TV (YouTube TV). <string name="revanced_spoof_client_use_android_tv_summary">"Spoof client to Android TV (YouTube TV).
Side effects include: Side effects include:
• No HDR video. • No HDR video.
• Audio track menu and playback speed menu are missing. • Audio track menu is missing.
• Captions may not be available. • Captions may not be available.
• Some live streams are not supported for playback."</string> • Some live streams are not supported for playback."</string>
<string name="revanced_spoof_client_use_android_vr_title">Android VR</string> <string name="revanced_spoof_client_use_android_vr_title">Android VR</string>