Skip to content

Commit

Permalink
feat(YouTube): add Spoof streaming data patch
Browse files Browse the repository at this point in the history
  • Loading branch information
inotia00 committed Aug 31, 2024
1 parent 0d29ed6 commit 15a9d7c
Show file tree
Hide file tree
Showing 15 changed files with 513 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ object VisualPreferencesIconsPatch : BaseResourcePatch(
"revanced_preference_screen_seekbar",
"revanced_preference_screen_settings_menu",
"revanced_preference_screen_shorts_player",
"revanced_preference_screen_spoof_streaming_data",
"revanced_preference_screen_toolbar",
"revanced_preference_screen_video_description",
"revanced_preference_screen_video_filter",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
package app.revanced.patches.youtube.utils.fix.streamingdata

import app.revanced.patcher.data.BytecodeContext
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.getInstructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.youtube.utils.compatibility.Constants
import app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints.BuildInitPlaybackRequestFingerprint
import app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints.BuildMediaDataSourceFingerprint
import app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints.BuildPlayerRequestURIFingerprint
import app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints.BuildRequestFingerprint
import app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints.CreateStreamingDataFingerprint
import app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints.NerdsStatsVideoFormatBuilderFingerprint
import app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints.ProtobufClassParseByteBufferFingerprint
import app.revanced.patches.youtube.utils.integrations.Constants.MISC_PATH
import app.revanced.patches.youtube.utils.settings.SettingsPatch
import app.revanced.util.getReference
import app.revanced.util.patch.BaseBytecodePatch
import app.revanced.util.resultOrThrow
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

object SpoofStreamingDataPatch : BaseBytecodePatch(
name = "Spoof streaming data",
description = "Adds options to spoof the streaming data to allow video playback.",
dependencies = setOf(
SettingsPatch::class,
SpoofUserAgentPatch::class,
),
compatiblePackages = Constants.COMPATIBLE_PACKAGE,
fingerprints = setOf(
BuildInitPlaybackRequestFingerprint,
BuildMediaDataSourceFingerprint,
BuildPlayerRequestURIFingerprint,
BuildRequestFingerprint,
CreateStreamingDataFingerprint,
ProtobufClassParseByteBufferFingerprint,

// Nerds stats video format.
NerdsStatsVideoFormatBuilderFingerprint,
)
) {
private const val INTEGRATIONS_CLASS_DESCRIPTOR =
"$MISC_PATH/SpoofStreamingDataPatch;"
private const val REQUEST_CLASS_DESCRIPTOR =
"Lorg/chromium/net/UrlRequest;"
private const val REQUEST_BUILDER_CLASS_DESCRIPTOR =
"Lorg/chromium/net/UrlRequest\$Builder;"

override fun execute(context: BytecodeContext) {

// region Block /initplayback requests to fall back to /get_watch requests.

BuildInitPlaybackRequestFingerprint.resultOrThrow().let {
it.mutableMethod.apply {
val moveUriStringIndex = it.scanResult.patternScanResult!!.startIndex
val targetRegister = getInstruction<OneRegisterInstruction>(moveUriStringIndex).registerA

addInstructions(
moveUriStringIndex + 1,
"""
invoke-static { v$targetRegister }, $INTEGRATIONS_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.resultOrThrow().let {
it.mutableMethod.apply {
val invokeToStringIndex = BuildPlayerRequestURIFingerprint.indexOfToStringInstruction(this)
val uriRegister = getInstruction<FiveRegisterInstruction>(invokeToStringIndex).registerC

addInstructions(
invokeToStringIndex,
"""
invoke-static { v$uriRegister }, $INTEGRATIONS_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri;
move-result-object v$uriRegister
""",
)
}
}

// endregion

// region Fetch replacement streams.

BuildRequestFingerprint.resultOrThrow().let { result ->
result.mutableMethod.apply {
val buildRequestIndex =
BuildRequestFingerprint.indexOfBuildUrlRequestInstruction(this)
val requestBuilderRegister = getInstruction<FiveRegisterInstruction>(buildRequestIndex).registerC

val newRequestBuilderIndex =
BuildRequestFingerprint.indexOfNewUrlRequestBuilderInstruction(this)
val urlRegister = getInstruction<FiveRegisterInstruction>(newRequestBuilderIndex).registerD

// Replace "requestBuilder.build()" with integrations call.
replaceInstruction(
buildRequestIndex,
"invoke-static { v$requestBuilderRegister }, " +
"$INTEGRATIONS_CLASS_DESCRIPTOR->" +
"buildRequest($REQUEST_BUILDER_CLASS_DESCRIPTOR)" +
REQUEST_CLASS_DESCRIPTOR
)

val entrySetIndex = BuildRequestFingerprint.indexOfEntrySetInstruction(this)
val mapRegister = if (entrySetIndex < 0)
urlRegister + 1
else
getInstruction<FiveRegisterInstruction>(entrySetIndex).registerC

var smaliInstructions =
"invoke-static { v$urlRegister, v$mapRegister }, " +
"$INTEGRATIONS_CLASS_DESCRIPTOR->" +
"setHeader(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.resultOrThrow().let { result ->
result.mutableMethod.apply {
val setStreamingDataIndex = result.scanResult.patternScanResult!!.startIndex
val setStreamingDataField = getInstruction(setStreamingDataIndex).getReference<FieldReference>().toString()

val playerProtoClass = getInstruction(setStreamingDataIndex + 1).getReference<FieldReference>()!!.definingClass
val protobufClass = ProtobufClassParseByteBufferFingerprint.resultOrThrow().mutableMethod.definingClass

val getStreamingDataField = getInstructions().find { instruction ->
instruction.opcode == Opcode.IGET_OBJECT &&
instruction.getReference<FieldReference>()?.definingClass == playerProtoClass
}?.getReference<FieldReference>()
?: throw PatchException("Could not find getStreamingDataField")

val videoDetailsIndex = result.scanResult.patternScanResult!!.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 { }, $INTEGRATIONS_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 }, $INTEGRATIONS_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 v0, :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.resultOrThrow().let {
it.mutableMethod.apply {
val targetIndex = getInstructions().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 }, $INTEGRATIONS_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.resultOrThrow().mutableMethod.apply {
for (index in implementation!!.instructions.size - 1 downTo 0) {
val instruction = getInstruction(index)
if (instruction.opcode != Opcode.RETURN_OBJECT)
continue

val register = (instruction as OneRegisterInstruction).registerA

addInstructions(
index, """
invoke-static {v$register}, $INTEGRATIONS_CLASS_DESCRIPTOR->appendSpoofedClient(Ljava/lang/String;)Ljava/lang/String;
move-result-object v$register
"""
)
}
}

// endregion

/**
* Add settings
*/
SettingsPatch.addPreference(
arrayOf(
"SETTINGS: SPOOF_STREAMING_DATA"
)
)

SettingsPatch.updatePatchStatus(this)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package app.revanced.patches.youtube.utils.fix.streamingdata

import app.revanced.patches.shared.spoofuseragent.BaseSpoofUserAgentPatch

object SpoofUserAgentPatch : BaseSpoofUserAgentPatch("com.google.android.youtube")
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints

import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.Opcode

internal object BuildInitPlaybackRequestFingerprint : MethodFingerprint(
returnType = "Lorg/chromium/net/UrlRequest\$Builder;",
opcodes = listOf(
Opcode.MOVE_RESULT_OBJECT,
Opcode.IGET_OBJECT, // Moves the request URI string to a register to build the request with.
),
strings = listOf(
"Content-Type",
"Range",
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints

import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.AccessFlags

internal object BuildMediaDataSourceFingerprint : MethodFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
returnType = "V",
parameters = listOf(
"Landroid/net/Uri;",
"J",
"I",
"[B",
"Ljava/util/Map;",
"J",
"J",
"Ljava/lang/String;",
"I",
"Ljava/lang/Object;"
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints

import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints.BuildPlayerRequestURIFingerprint.indexOfToStringInstruction
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.MethodReference

internal object BuildPlayerRequestURIFingerprint : MethodFingerprint(
returnType = "Ljava/lang/String;",
strings = listOf(
"key",
"asig",
),
customFingerprint = { methodDef, _ ->
indexOfToStringInstruction(methodDef) >= 0
},
) {
fun indexOfToStringInstruction(methodDef: Method) =
methodDef.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>().toString() == "Landroid/net/Uri;->toString()Ljava/lang/String;"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints

import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints.BuildRequestFingerprint.indexOfBuildUrlRequestInstruction
import app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints.BuildRequestFingerprint.indexOfEntrySetInstruction
import app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints.BuildRequestFingerprint.indexOfNewUrlRequestBuilderInstruction
import app.revanced.patches.youtube.utils.fix.streamingdata.fingerprints.BuildRequestFingerprint.indexOfRequestFinishedListenerInstruction
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.MethodReference

internal object BuildRequestFingerprint : MethodFingerprint(
customFingerprint = { methodDef, _ ->
methodDef.implementation != null &&
indexOfRequestFinishedListenerInstruction(methodDef) >= 0 &&
!methodDef.definingClass.startsWith("Lorg/") &&
indexOfNewUrlRequestBuilderInstruction(methodDef) >= 0 &&
indexOfBuildUrlRequestInstruction(methodDef) >= 0 &&
// YouTube 17.34.36 ~ YouTube 18.35.36
(indexOfEntrySetInstruction(methodDef) >= 0 ||
// YouTube 18.36.39 ~
methodDef.parameters[1].type == "Ljava/util/Map;")
}
) {
fun indexOfRequestFinishedListenerInstruction(methodDef: Method) =
methodDef.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>().toString() == "Lorg/chromium/net/ExperimentalUrlRequest${'$'}Builder;->setRequestFinishedListener(Lorg/chromium/net/RequestFinishedInfo${'$'}Listener;)Lorg/chromium/net/ExperimentalUrlRequest${'$'}Builder;"
}

fun indexOfNewUrlRequestBuilderInstruction(methodDef: Method) =
methodDef.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;"
}

fun indexOfBuildUrlRequestInstruction(methodDef: Method) =
methodDef.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>().toString() == "Lorg/chromium/net/ExperimentalUrlRequest${'$'}Builder;->build()Lorg/chromium/net/ExperimentalUrlRequest;"
}

fun indexOfEntrySetInstruction(methodDef: Method) =
methodDef.indexOfFirstInstruction {
opcode == Opcode.INVOKE_INTERFACE &&
getReference<MethodReference>().toString() == "Ljava/util/Map;->entrySet()Ljava/util/Set;"
}
}
Loading

0 comments on commit 15a9d7c

Please sign in to comment.