mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-06-12 05:07:41 +02:00
feat(YouTube Music): Add Spoof streaming data
patch
This commit is contained in:
@ -46,6 +46,7 @@ private const val CLIENT_INFO_CLASS_DESCRIPTOR =
|
||||
val spoofClientPatch = bytecodePatch(
|
||||
SPOOF_CLIENT.title,
|
||||
SPOOF_CLIENT.summary,
|
||||
false,
|
||||
) {
|
||||
dependsOn(settingsPatch)
|
||||
|
||||
|
@ -0,0 +1,58 @@
|
||||
package app.revanced.patches.music.utils.fix.streamingdata
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||
import app.revanced.patches.music.utils.compatibility.Constants.YOUTUBE_MUSIC_PACKAGE_NAME
|
||||
import app.revanced.patches.music.utils.patch.PatchList.SPOOF_STREAMING_DATA
|
||||
import app.revanced.patches.music.utils.settings.CategoryType
|
||||
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
|
||||
import app.revanced.patches.music.utils.settings.addSwitchPreference
|
||||
import app.revanced.patches.music.utils.settings.settingsPatch
|
||||
import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
|
||||
import app.revanced.patches.shared.spoof.streamingdata.baseSpoofStreamingDataPatch
|
||||
import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch
|
||||
import app.revanced.util.findMethodOrThrow
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
|
||||
private const val DEFAULT_CLIENT_TYPE = "ANDROID_VR"
|
||||
|
||||
val spoofStreamingDataPatch = baseSpoofStreamingDataPatch(
|
||||
{
|
||||
compatibleWith(COMPATIBLE_PACKAGE)
|
||||
|
||||
dependsOn(
|
||||
baseSpoofUserAgentPatch(YOUTUBE_MUSIC_PACKAGE_NAME),
|
||||
settingsPatch,
|
||||
)
|
||||
},
|
||||
{
|
||||
findMethodOrThrow("$PATCHES_PATH/PatchStatus;") {
|
||||
name == "SpoofStreamingDataDefaultClient"
|
||||
}.apply {
|
||||
val register = getInstruction<OneRegisterInstruction>(0).registerA
|
||||
val type = (getInstruction<ReferenceInstruction>(0).reference as FieldReference).type
|
||||
replaceInstruction(
|
||||
0,
|
||||
"sget-object v$register, $type->$DEFAULT_CLIENT_TYPE:$type"
|
||||
)
|
||||
}
|
||||
|
||||
addSwitchPreference(
|
||||
CategoryType.MISC,
|
||||
"revanced_spoof_streaming_data",
|
||||
"true"
|
||||
)
|
||||
addSwitchPreference(
|
||||
CategoryType.MISC,
|
||||
"revanced_spoof_streaming_data_stats_for_nerds",
|
||||
"true",
|
||||
"revanced_spoof_streaming_data"
|
||||
)
|
||||
|
||||
updatePatchStatus(SPOOF_STREAMING_DATA)
|
||||
|
||||
}
|
||||
)
|
@ -143,7 +143,11 @@ internal enum class PatchList(
|
||||
),
|
||||
SPOOF_CLIENT(
|
||||
"Spoof client",
|
||||
"Adds options to spoof the client to allow track playback."
|
||||
"Adds options to spoof the client to allow playback."
|
||||
),
|
||||
SPOOF_STREAMING_DATA(
|
||||
"Spoof streaming data",
|
||||
"Adds options to spoof the streaming data to allow playback."
|
||||
),
|
||||
TRANSLATIONS_FOR_YOUTUBE_MUSIC(
|
||||
"Translations for YouTube Music",
|
||||
|
@ -6,6 +6,7 @@ internal object Constants {
|
||||
const val PATCHES_PATH = "$EXTENSION_PATH/patches"
|
||||
const val COMPONENTS_PATH = "$PATCHES_PATH/components"
|
||||
const val SPANS_PATH = "$PATCHES_PATH/spans"
|
||||
const val SPOOF_PATH = "$PATCHES_PATH/spoof"
|
||||
|
||||
const val EXTENSION_UTILS_PATH = "$EXTENSION_PATH/utils"
|
||||
const val EXTENSION_SETTING_CLASS_DESCRIPTOR = "$EXTENSION_PATH/settings/Setting;"
|
||||
|
@ -0,0 +1,215 @@
|
||||
package app.revanced.patches.shared.spoof.streamingdata
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.instructions
|
||||
import app.revanced.patcher.patch.BytecodePatchBuilder
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.util.smali.ExternalLabel
|
||||
import app.revanced.patches.shared.extension.Constants.SPOOF_PATH
|
||||
import app.revanced.util.findInstructionIndicesReversedOrThrow
|
||||
import app.revanced.util.fingerprint.definingClassOrThrow
|
||||
import app.revanced.util.fingerprint.matchOrThrow
|
||||
import app.revanced.util.fingerprint.methodOrThrow
|
||||
import app.revanced.util.getReference
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
|
||||
const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"$SPOOF_PATH/SpoofStreamingDataPatch;"
|
||||
|
||||
fun baseSpoofStreamingDataPatch(
|
||||
block: BytecodePatchBuilder.() -> Unit = {},
|
||||
executeBlock: BytecodePatchContext.() -> Unit = {},
|
||||
) = bytecodePatch(
|
||||
name = "Spoof streaming data",
|
||||
description = "Adds options to spoof the streaming data to allow playback."
|
||||
) {
|
||||
block()
|
||||
|
||||
execute {
|
||||
// region Block /initplayback requests to fall back to /get_watch requests.
|
||||
|
||||
buildInitPlaybackRequestFingerprint.matchOrThrow().let {
|
||||
it.method.apply {
|
||||
val moveUriStringIndex = it.patternMatch!!.startIndex
|
||||
val targetRegister =
|
||||
getInstruction<OneRegisterInstruction>(moveUriStringIndex).registerA
|
||||
|
||||
addInstructions(
|
||||
moveUriStringIndex + 1,
|
||||
"""
|
||||
invoke-static { v$targetRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockInitPlaybackRequest(Ljava/lang/String;)Ljava/lang/String;
|
||||
move-result-object v$targetRegister
|
||||
""",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Block /get_watch requests to fall back to /player requests.
|
||||
|
||||
buildPlayerRequestURIFingerprint.methodOrThrow().apply {
|
||||
val invokeToStringIndex = indexOfToStringInstruction(this)
|
||||
val uriRegister =
|
||||
getInstruction<FiveRegisterInstruction>(invokeToStringIndex).registerC
|
||||
|
||||
addInstructions(
|
||||
invokeToStringIndex,
|
||||
"""
|
||||
invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri;
|
||||
move-result-object v$uriRegister
|
||||
""",
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Get replacement streams at player requests.
|
||||
|
||||
buildRequestFingerprint.methodOrThrow().apply {
|
||||
val newRequestBuilderIndex = indexOfNewUrlRequestBuilderInstruction(this)
|
||||
val urlRegister =
|
||||
getInstruction<FiveRegisterInstruction>(newRequestBuilderIndex).registerD
|
||||
|
||||
val entrySetIndex = indexOfEntrySetInstruction(this)
|
||||
val mapRegister = if (entrySetIndex < 0)
|
||||
urlRegister + 1
|
||||
else
|
||||
getInstruction<FiveRegisterInstruction>(entrySetIndex).registerC
|
||||
|
||||
var smaliInstructions =
|
||||
"invoke-static { v$urlRegister, v$mapRegister }, " +
|
||||
"$EXTENSION_CLASS_DESCRIPTOR->" +
|
||||
"fetchStreams(Ljava/lang/String;Ljava/util/Map;)V"
|
||||
|
||||
if (entrySetIndex < 0) smaliInstructions = """
|
||||
move-object/from16 v$mapRegister, p1
|
||||
|
||||
""" + smaliInstructions
|
||||
|
||||
// Copy request headers for streaming data fetch.
|
||||
addInstructions(newRequestBuilderIndex + 2, smaliInstructions)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Replace the streaming data.
|
||||
|
||||
createStreamingDataFingerprint.matchOrThrow(createStreamingDataParentFingerprint).let { result ->
|
||||
result.method.apply {
|
||||
val setStreamingDataIndex = result.patternMatch!!.startIndex
|
||||
val setStreamingDataField =
|
||||
getInstruction(setStreamingDataIndex).getReference<FieldReference>().toString()
|
||||
|
||||
val playerProtoClass =
|
||||
getInstruction(setStreamingDataIndex + 1).getReference<FieldReference>()!!.definingClass
|
||||
val protobufClass =
|
||||
protobufClassParseByteBufferFingerprint.definingClassOrThrow()
|
||||
|
||||
val getStreamingDataField = instructions.find { instruction ->
|
||||
instruction.opcode == Opcode.IGET_OBJECT &&
|
||||
instruction.getReference<FieldReference>()?.definingClass == playerProtoClass
|
||||
}?.getReference<FieldReference>()
|
||||
?: throw PatchException("Could not find getStreamingDataField")
|
||||
|
||||
val videoDetailsIndex = result.patternMatch!!.endIndex
|
||||
val videoDetailsClass =
|
||||
getInstruction(videoDetailsIndex).getReference<FieldReference>()!!.type
|
||||
|
||||
val insertIndex = videoDetailsIndex + 1
|
||||
val videoDetailsRegister =
|
||||
getInstruction<TwoRegisterInstruction>(videoDetailsIndex).registerA
|
||||
|
||||
val overrideRegister = getInstruction<TwoRegisterInstruction>(insertIndex).registerA
|
||||
val freeRegister = implementation!!.registerCount - parameters.size - 2
|
||||
|
||||
addInstructionsWithLabels(
|
||||
insertIndex,
|
||||
"""
|
||||
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z
|
||||
move-result v$freeRegister
|
||||
if-eqz v$freeRegister, :disabled
|
||||
|
||||
# Get video id.
|
||||
# From YouTube 17.34.36 to YouTube 19.16.39, the field names and field types are the same.
|
||||
iget-object v$freeRegister, v$videoDetailsRegister, $videoDetailsClass->c:Ljava/lang/String;
|
||||
if-eqz v$freeRegister, :disabled
|
||||
|
||||
# Get streaming data.
|
||||
invoke-static { v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
|
||||
move-result-object v$freeRegister
|
||||
if-eqz v$freeRegister, :disabled
|
||||
|
||||
# Parse streaming data.
|
||||
sget-object v$overrideRegister, $playerProtoClass->a:$playerProtoClass
|
||||
invoke-static { v$overrideRegister, v$freeRegister }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass
|
||||
move-result-object v$freeRegister
|
||||
check-cast v$freeRegister, $playerProtoClass
|
||||
|
||||
# Set streaming data.
|
||||
iget-object v$freeRegister, v$freeRegister, $getStreamingDataField
|
||||
if-eqz v$freeRegister, :disabled
|
||||
iput-object v$freeRegister, p0, $setStreamingDataField
|
||||
|
||||
""",
|
||||
ExternalLabel("disabled", getInstruction(insertIndex))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Remove /videoplayback request body to fix playback.
|
||||
// This is needed when using iOS client as streaming data source.
|
||||
|
||||
buildMediaDataSourceFingerprint.methodOrThrow().apply {
|
||||
val targetIndex = instructions.lastIndex
|
||||
|
||||
addInstructions(
|
||||
targetIndex,
|
||||
"""
|
||||
# Field a: Stream uri.
|
||||
# Field c: Http method.
|
||||
# Field d: Post data.
|
||||
move-object/from16 v0, p0
|
||||
iget-object v1, v0, $definingClass->a:Landroid/net/Uri;
|
||||
iget v2, v0, $definingClass->c:I
|
||||
iget-object v3, v0, $definingClass->d:[B
|
||||
invoke-static { v1, v2, v3 }, $EXTENSION_CLASS_DESCRIPTOR->removeVideoPlaybackPostBody(Landroid/net/Uri;I[B)[B
|
||||
move-result-object v1
|
||||
iput-object v1, v0, $definingClass->d:[B
|
||||
""",
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Append spoof info.
|
||||
|
||||
nerdsStatsVideoFormatBuilderFingerprint.methodOrThrow().apply {
|
||||
findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index ->
|
||||
val register = getInstruction<OneRegisterInstruction>(index).registerA
|
||||
|
||||
addInstructions(
|
||||
index, """
|
||||
invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->appendSpoofedClient(Ljava/lang/String;)Ljava/lang/String;
|
||||
move-result-object v$register
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
executeBlock()
|
||||
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.patches.youtube.utils.fix.streamingdata
|
||||
package app.revanced.patches.shared.spoof.streamingdata
|
||||
|
||||
import app.revanced.util.fingerprint.legacyFingerprint
|
||||
import app.revanced.util.getReference
|
||||
@ -10,38 +10,6 @@ import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
|
||||
internal val buildBrowseRequestFingerprint = legacyFingerprint(
|
||||
name = "buildBrowseRequestFingerprint",
|
||||
customFingerprint = { method, _ ->
|
||||
method.implementation != null &&
|
||||
indexOfRequestFinishedListenerInstruction(method) >= 0 &&
|
||||
!method.definingClass.startsWith("Lorg/") &&
|
||||
indexOfNewUrlRequestBuilderInstruction(method) >= 0 &&
|
||||
// YouTube 17.34.36 ~ YouTube 18.35.36
|
||||
(indexOfEntrySetInstruction(method) >= 0 ||
|
||||
// YouTube 18.36.39 ~
|
||||
method.parameters[1].type == "Ljava/util/Map;")
|
||||
}
|
||||
)
|
||||
|
||||
internal fun indexOfRequestFinishedListenerInstruction(method: Method) =
|
||||
method.indexOfFirstInstruction {
|
||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||
getReference<MethodReference>()?.name == "setRequestFinishedListener"
|
||||
}
|
||||
|
||||
internal fun indexOfNewUrlRequestBuilderInstruction(method: Method) =
|
||||
method.indexOfFirstInstruction {
|
||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||
getReference<MethodReference>().toString() == "Lorg/chromium/net/CronetEngine;->newUrlRequestBuilder(Ljava/lang/String;Lorg/chromium/net/UrlRequest${'$'}Callback;Ljava/util/concurrent/Executor;)Lorg/chromium/net/UrlRequest${'$'}Builder;"
|
||||
}
|
||||
|
||||
internal fun indexOfEntrySetInstruction(method: Method) =
|
||||
method.indexOfFirstInstruction {
|
||||
opcode == Opcode.INVOKE_INTERFACE &&
|
||||
getReference<MethodReference>().toString() == "Ljava/util/Map;->entrySet()Ljava/util/Set;"
|
||||
}
|
||||
|
||||
internal val buildInitPlaybackRequestFingerprint = legacyFingerprint(
|
||||
name = "buildInitPlaybackRequestFingerprint",
|
||||
returnType = "Lorg/chromium/net/UrlRequest\$Builder;",
|
||||
@ -91,6 +59,38 @@ internal fun indexOfToStringInstruction(method: Method) =
|
||||
getReference<MethodReference>().toString() == "Landroid/net/Uri;->toString()Ljava/lang/String;"
|
||||
}
|
||||
|
||||
internal val buildRequestFingerprint = legacyFingerprint(
|
||||
name = "buildRequestFingerprint",
|
||||
customFingerprint = { method, _ ->
|
||||
method.implementation != null &&
|
||||
indexOfRequestFinishedListenerInstruction(method) >= 0 &&
|
||||
!method.definingClass.startsWith("Lorg/") &&
|
||||
indexOfNewUrlRequestBuilderInstruction(method) >= 0 &&
|
||||
// Earlier targets
|
||||
(indexOfEntrySetInstruction(method) >= 0 ||
|
||||
// Later targets
|
||||
method.parameters[1].type == "Ljava/util/Map;")
|
||||
}
|
||||
)
|
||||
|
||||
internal fun indexOfRequestFinishedListenerInstruction(method: Method) =
|
||||
method.indexOfFirstInstruction {
|
||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||
getReference<MethodReference>()?.name == "setRequestFinishedListener"
|
||||
}
|
||||
|
||||
internal fun indexOfNewUrlRequestBuilderInstruction(method: Method) =
|
||||
method.indexOfFirstInstruction {
|
||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||
getReference<MethodReference>().toString() == "Lorg/chromium/net/CronetEngine;->newUrlRequestBuilder(Ljava/lang/String;Lorg/chromium/net/UrlRequest${'$'}Callback;Ljava/util/concurrent/Executor;)Lorg/chromium/net/UrlRequest${'$'}Builder;"
|
||||
}
|
||||
|
||||
internal fun indexOfEntrySetInstruction(method: Method) =
|
||||
method.indexOfFirstInstruction {
|
||||
opcode == Opcode.INVOKE_INTERFACE &&
|
||||
getReference<MethodReference>().toString() == "Ljava/util/Map;->entrySet()Ljava/util/Set;"
|
||||
}
|
||||
|
||||
internal val createStreamingDataFingerprint = legacyFingerprint(
|
||||
name = "createStreamingDataFingerprint",
|
||||
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
|
||||
@ -103,19 +103,21 @@ internal val createStreamingDataFingerprint = legacyFingerprint(
|
||||
Opcode.SGET_OBJECT,
|
||||
Opcode.IPUT_OBJECT
|
||||
),
|
||||
customFingerprint = { method, _ ->
|
||||
method.indexOfFirstInstruction {
|
||||
opcode == Opcode.SGET_OBJECT &&
|
||||
getReference<FieldReference>()?.name == "playerThreedRenderer"
|
||||
} >= 0
|
||||
},
|
||||
)
|
||||
|
||||
internal val createStreamingDataParentFingerprint = legacyFingerprint(
|
||||
name = "createStreamingDataParentFingerprint",
|
||||
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
|
||||
returnType = "L",
|
||||
parameters = emptyList(),
|
||||
strings = listOf("Invalid playback type; streaming data is not playable"),
|
||||
)
|
||||
|
||||
internal val nerdsStatsVideoFormatBuilderFingerprint = legacyFingerprint(
|
||||
name = "nerdsStatsVideoFormatBuilderFingerprint",
|
||||
returnType = "Ljava/lang/String;",
|
||||
accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC,
|
||||
parameters = listOf("Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;"),
|
||||
parameters = listOf("L"),
|
||||
strings = listOf("codecs=\""),
|
||||
)
|
||||
|
@ -1,222 +1,23 @@
|
||||
package app.revanced.patches.youtube.utils.fix.streamingdata
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.instructions
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.util.smali.ExternalLabel
|
||||
import app.revanced.patches.shared.spoof.streamingdata.baseSpoofStreamingDataPatch
|
||||
import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch
|
||||
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||
import app.revanced.patches.youtube.utils.extension.Constants.MISC_PATH
|
||||
import app.revanced.patches.youtube.utils.compatibility.Constants.YOUTUBE_PACKAGE_NAME
|
||||
import app.revanced.patches.youtube.utils.patch.PatchList.SPOOF_STREAMING_DATA
|
||||
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
|
||||
import app.revanced.patches.youtube.utils.settings.settingsPatch
|
||||
import app.revanced.util.findInstructionIndicesReversedOrThrow
|
||||
import app.revanced.util.fingerprint.definingClassOrThrow
|
||||
import app.revanced.util.fingerprint.matchOrThrow
|
||||
import app.revanced.util.fingerprint.methodOrThrow
|
||||
import app.revanced.util.getReference
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"$MISC_PATH/SpoofStreamingDataPatch;"
|
||||
|
||||
val spoofStreamingDataPatch = bytecodePatch(
|
||||
SPOOF_STREAMING_DATA.title,
|
||||
SPOOF_STREAMING_DATA.summary,
|
||||
) {
|
||||
compatibleWith(COMPATIBLE_PACKAGE)
|
||||
|
||||
dependsOn(
|
||||
baseSpoofUserAgentPatch("com.google.android.youtube"),
|
||||
settingsPatch
|
||||
)
|
||||
|
||||
execute {
|
||||
// region Block /get_watch requests to fall back to /player requests.
|
||||
|
||||
buildPlayerRequestURIFingerprint.methodOrThrow().apply {
|
||||
val invokeToStringIndex = indexOfToStringInstruction(this)
|
||||
val uriRegister =
|
||||
getInstruction<FiveRegisterInstruction>(invokeToStringIndex).registerC
|
||||
|
||||
addInstructions(
|
||||
invokeToStringIndex,
|
||||
"""
|
||||
invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri;
|
||||
move-result-object v$uriRegister
|
||||
""",
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Block /initplayback requests to fall back to /get_watch requests.
|
||||
|
||||
buildInitPlaybackRequestFingerprint.matchOrThrow().let {
|
||||
it.method.apply {
|
||||
val moveUriStringIndex = it.patternMatch!!.startIndex
|
||||
val targetRegister =
|
||||
getInstruction<OneRegisterInstruction>(moveUriStringIndex).registerA
|
||||
|
||||
addInstructions(
|
||||
moveUriStringIndex + 1,
|
||||
"""
|
||||
invoke-static { v$targetRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockInitPlaybackRequest(Ljava/lang/String;)Ljava/lang/String;
|
||||
move-result-object v$targetRegister
|
||||
""",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Fetch replacement streams.
|
||||
|
||||
buildBrowseRequestFingerprint.methodOrThrow().apply {
|
||||
val newRequestBuilderIndex = indexOfNewUrlRequestBuilderInstruction(this)
|
||||
val urlRegister =
|
||||
getInstruction<FiveRegisterInstruction>(newRequestBuilderIndex).registerD
|
||||
|
||||
val entrySetIndex = indexOfEntrySetInstruction(this)
|
||||
val mapRegister = if (entrySetIndex < 0)
|
||||
urlRegister + 1
|
||||
else
|
||||
getInstruction<FiveRegisterInstruction>(entrySetIndex).registerC
|
||||
|
||||
var smaliInstructions =
|
||||
"invoke-static { v$urlRegister, v$mapRegister }, " +
|
||||
"$EXTENSION_CLASS_DESCRIPTOR->" +
|
||||
"fetchStreams(Ljava/lang/String;Ljava/util/Map;)V"
|
||||
|
||||
if (entrySetIndex < 0) smaliInstructions = """
|
||||
move-object/from16 v$mapRegister, p1
|
||||
|
||||
""" + smaliInstructions
|
||||
|
||||
// Copy request headers for streaming data fetch.
|
||||
addInstructions(newRequestBuilderIndex + 2, smaliInstructions)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Replace the streaming data.
|
||||
|
||||
createStreamingDataFingerprint.matchOrThrow().let { result ->
|
||||
result.method.apply {
|
||||
val setStreamingDataIndex = result.patternMatch!!.startIndex
|
||||
val setStreamingDataField =
|
||||
getInstruction(setStreamingDataIndex).getReference<FieldReference>().toString()
|
||||
|
||||
val playerProtoClass =
|
||||
getInstruction(setStreamingDataIndex + 1).getReference<FieldReference>()!!.definingClass
|
||||
val protobufClass =
|
||||
protobufClassParseByteBufferFingerprint.definingClassOrThrow()
|
||||
|
||||
val getStreamingDataField = instructions.find { instruction ->
|
||||
instruction.opcode == Opcode.IGET_OBJECT &&
|
||||
instruction.getReference<FieldReference>()?.definingClass == playerProtoClass
|
||||
}?.getReference<FieldReference>()
|
||||
?: throw PatchException("Could not find getStreamingDataField")
|
||||
|
||||
val videoDetailsIndex = result.patternMatch!!.endIndex
|
||||
val videoDetailsClass =
|
||||
getInstruction(videoDetailsIndex).getReference<FieldReference>()!!.type
|
||||
|
||||
val insertIndex = videoDetailsIndex + 1
|
||||
val videoDetailsRegister =
|
||||
getInstruction<TwoRegisterInstruction>(videoDetailsIndex).registerA
|
||||
|
||||
val overrideRegister = getInstruction<TwoRegisterInstruction>(insertIndex).registerA
|
||||
val freeRegister = implementation!!.registerCount - parameters.size - 2
|
||||
|
||||
addInstructionsWithLabels(
|
||||
insertIndex,
|
||||
"""
|
||||
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z
|
||||
move-result v$freeRegister
|
||||
if-eqz v$freeRegister, :disabled
|
||||
|
||||
# Get video id.
|
||||
# From YouTube 17.34.36 to YouTube 19.16.39, the field names and field types are the same.
|
||||
iget-object v$freeRegister, v$videoDetailsRegister, $videoDetailsClass->c:Ljava/lang/String;
|
||||
if-eqz v$freeRegister, :disabled
|
||||
|
||||
# Get streaming data.
|
||||
invoke-static { v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
|
||||
move-result-object v$freeRegister
|
||||
if-eqz v$freeRegister, :disabled
|
||||
|
||||
# Parse streaming data.
|
||||
sget-object v$overrideRegister, $playerProtoClass->a:$playerProtoClass
|
||||
invoke-static { v$overrideRegister, v$freeRegister }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass
|
||||
move-result-object v$freeRegister
|
||||
check-cast v$freeRegister, $playerProtoClass
|
||||
|
||||
# Set streaming data.
|
||||
iget-object v$freeRegister, v$freeRegister, $getStreamingDataField
|
||||
if-eqz v$freeRegister, :disabled
|
||||
iput-object v$freeRegister, p0, $setStreamingDataField
|
||||
|
||||
""",
|
||||
ExternalLabel("disabled", getInstruction(insertIndex))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Remove /videoplayback request body to fix playback.
|
||||
// This is needed when using iOS client as streaming data source.
|
||||
|
||||
buildMediaDataSourceFingerprint.methodOrThrow().apply {
|
||||
val targetIndex = instructions.lastIndex
|
||||
|
||||
addInstructions(
|
||||
targetIndex,
|
||||
"""
|
||||
# Field a: Stream uri.
|
||||
# Field c: Http method.
|
||||
# Field d: Post data.
|
||||
# From YouTube 17.34.36 to YouTube 19.16.39, the field names and field types are the same.
|
||||
move-object/from16 v0, p0
|
||||
iget-object v1, v0, $definingClass->a:Landroid/net/Uri;
|
||||
iget v2, v0, $definingClass->c:I
|
||||
iget-object v3, v0, $definingClass->d:[B
|
||||
invoke-static { v1, v2, v3 }, $EXTENSION_CLASS_DESCRIPTOR->removeVideoPlaybackPostBody(Landroid/net/Uri;I[B)[B
|
||||
move-result-object v1
|
||||
iput-object v1, v0, $definingClass->d:[B
|
||||
""",
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Append spoof info.
|
||||
|
||||
nerdsStatsVideoFormatBuilderFingerprint.methodOrThrow().apply {
|
||||
findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index ->
|
||||
val register = getInstruction<OneRegisterInstruction>(index).registerA
|
||||
|
||||
addInstructions(
|
||||
index, """
|
||||
invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->appendSpoofedClient(Ljava/lang/String;)Ljava/lang/String;
|
||||
move-result-object v$register
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region add settings
|
||||
val spoofStreamingDataPatch = baseSpoofStreamingDataPatch(
|
||||
{
|
||||
compatibleWith(COMPATIBLE_PACKAGE)
|
||||
|
||||
dependsOn(
|
||||
baseSpoofUserAgentPatch(YOUTUBE_PACKAGE_NAME),
|
||||
settingsPatch
|
||||
)
|
||||
},
|
||||
{
|
||||
addPreference(
|
||||
arrayOf(
|
||||
"SETTINGS: SPOOF_STREAMING_DATA"
|
||||
@ -224,7 +25,5 @@ val spoofStreamingDataPatch = bytecodePatch(
|
||||
SPOOF_STREAMING_DATA
|
||||
)
|
||||
|
||||
// endregion
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -223,7 +223,7 @@ internal enum class PatchList(
|
||||
),
|
||||
SPOOF_STREAMING_DATA(
|
||||
"Spoof streaming data",
|
||||
"Adds options to spoof the streaming data to allow video playback."
|
||||
"Adds options to spoof the streaming data to allow playback."
|
||||
),
|
||||
SWIPE_CONTROLS(
|
||||
"Swipe controls",
|
||||
|
@ -55,6 +55,10 @@ internal fun Pair<String, Fingerprint>.matchOrNull(parentFingerprint: Pair<Strin
|
||||
second.matchOrNull(parentClassDef)
|
||||
}
|
||||
|
||||
context(BytecodePatchContext)
|
||||
internal fun Pair<String, Fingerprint>.methodOrNull(): MutableMethod? =
|
||||
matchOrNull()?.method
|
||||
|
||||
context(BytecodePatchContext)
|
||||
internal fun Pair<String, Fingerprint>.methodOrThrow(): MutableMethod =
|
||||
second.methodOrNull ?: throw first.exception
|
||||
@ -63,6 +67,14 @@ context(BytecodePatchContext)
|
||||
internal fun Pair<String, Fingerprint>.methodOrThrow(parentFingerprint: Pair<String, Fingerprint>): MutableMethod =
|
||||
matchOrThrow(parentFingerprint).method
|
||||
|
||||
context(BytecodePatchContext)
|
||||
internal fun Pair<String, Fingerprint>.originalMethodOrThrow(): Method =
|
||||
second.originalMethodOrNull ?: throw first.exception
|
||||
|
||||
context(BytecodePatchContext)
|
||||
internal fun Pair<String, Fingerprint>.originalMethodOrThrow(parentFingerprint: Pair<String, Fingerprint>): Method =
|
||||
matchOrThrow(parentFingerprint).originalMethod
|
||||
|
||||
context(BytecodePatchContext)
|
||||
internal fun Pair<String, Fingerprint>.mutableClassOrThrow(): MutableClass =
|
||||
second.classDefOrNull ?: throw first.exception
|
||||
|
Reference in New Issue
Block a user