From 5c7c8b536416ec53cd98f7d59d11850aa1b70f11 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 20 Sep 2025 14:36:15 +0400 Subject: [PATCH] fix(Spoof video streams): Resolve occasional playback stuttering Code adapted from: https://github.com/inotia00/revanced-patches/commit/2cf9db66ac3f241d74cbbe2579961f4e7c57f8ae https://github.com/inotia00/revanced-patches/commit/50d9c60374d8c2ee4f8034b3cbcb1d68e59b6a84 --- .../shared/spoof/SpoofVideoStreamsPatch.java | 39 +++++++- .../patches/shared/misc/spoof/Fingerprints.kt | 19 ++-- .../misc/spoof/SpoofVideoStreamsPatch.kt | 89 +++++++++++++++++-- 3 files changed, 132 insertions(+), 15 deletions(-) diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java index f11c7ea26..a5f69ba30 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java @@ -97,6 +97,35 @@ public class SpoofVideoStreamsPatch { return playerRequestUri; } + /** + * Injection point. + * + * Blocks /get_watch requests by returning an unreachable URI. + * /att/get requests are used to obtain a PoToken challenge. + * See: botGuardScript.js#L15 + *

+ * Since the Spoof streaming data patch was implemented because a valid PoToken cannot be obtained, + * Blocking /att/get requests are not a problem. + */ + public static String blockGetAttRequest(String originalUrlString) { + if (SPOOF_STREAMING_DATA) { + try { + var originalUri = Uri.parse(originalUrlString); + String path = originalUri.getPath(); + + if (path != null && path.contains("att/get")) { + Logger.printDebug(() -> "Blocking 'att/get' by returning internet connection check uri"); + + return INTERNET_CONNECTION_CHECK_URI_STRING; + } + } catch (Exception ex) { + Logger.printException(() -> "blockGetAttRequest failure", ex); + } + } + + return originalUrlString; + } + /** * Injection point. *

@@ -130,7 +159,7 @@ public class SpoofVideoStreamsPatch { /** * Injection point. - * Only invoked when playing a livestream on an iOS client. + * Only invoked when playing a livestream on an Apple client. */ public static boolean fixHLSCurrentTime(boolean original) { if (!SPOOF_STREAMING_DATA) { @@ -139,6 +168,14 @@ public class SpoofVideoStreamsPatch { return false; } + /* + * Injection point. + * Fix audio stuttering in YouTube Music. + */ + public static boolean disableSABR() { + return SPOOF_STREAMING_DATA; + } + /** * Injection point. * Turns off a feature flag that interferes with spoofing. diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/Fingerprints.kt index ed2f86387..7c42648c7 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/Fingerprints.kt @@ -1,12 +1,9 @@ package app.revanced.patches.shared.misc.spoof import app.revanced.patcher.fingerprint -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstruction import app.revanced.util.literal import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.reference.MethodReference internal val buildInitPlaybackRequestFingerprint = fingerprint { returns("Lorg/chromium/net/UrlRequest\$Builder;") @@ -40,10 +37,7 @@ internal val buildRequestFingerprint = fingerprint { accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) returns("Lorg/chromium/net/UrlRequest") // UrlRequest; or UrlRequest$Builder; custom { methodDef, _ -> - if (methodDef.indexOfFirstInstruction { - val reference = getReference() - reference?.name == "newUrlRequestBuilder" - } < 0) { + if (indexOfNewUrlRequestBuilderInstruction(methodDef) < 0) { return@custom false } @@ -142,6 +136,17 @@ internal val hlsCurrentTimeFingerprint = fingerprint { } } +internal const val DISABLED_BY_SABR_STREAMING_URI_STRING = "DISABLED_BY_SABR_STREAMING_URI" + +internal val mediaFetchEnumConstructorFingerprint = fingerprint { + returns("V") + strings( + "ENABLED", + "DISABLED_FOR_PLAYBACK", + DISABLED_BY_SABR_STREAMING_URI_STRING + ) +} + internal val nerdsStatsVideoFormatBuilderFingerprint = fingerprint { accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) returns("Ljava/lang/String;") diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt index 26b80a2f9..40746553f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt @@ -5,23 +5,28 @@ 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.fingerprint import app.revanced.patcher.patch.BytecodePatchBuilder import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.util.findFreeRegister import app.revanced.util.findInstructionIndicesReversedOrThrow import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.insertLiteralOverride import app.revanced.util.returnEarly import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.Method 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.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference @@ -31,6 +36,9 @@ import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/shared/spoof/SpoofVideoStreamsPatch;" +private lateinit var buildRequestMethod: MutableMethod +private var buildRequestMethodUrlRegister = -1 + fun spoofVideoStreamsPatch( block: BytecodePatchBuilder.() -> Unit = {}, fixMediaFetchHotConfigChanges: BytecodePatchBuilder.() -> Boolean = { false }, @@ -91,18 +99,17 @@ fun spoofVideoStreamsPatch( // region Get replacement streams at player requests. buildRequestFingerprint.method.apply { - val newRequestBuilderIndex = indexOfFirstInstructionOrThrow { - opcode == Opcode.INVOKE_VIRTUAL && - getReference()?.name == "newUrlRequestBuilder" - } - val urlRegister = getInstruction(newRequestBuilderIndex).registerD - val freeRegister = findFreeRegister(newRequestBuilderIndex, urlRegister) + buildRequestMethod = this + + val newRequestBuilderIndex = indexOfNewUrlRequestBuilderInstruction(this) + buildRequestMethodUrlRegister = getInstruction(newRequestBuilderIndex).registerD + val freeRegister = findFreeRegister(newRequestBuilderIndex, buildRequestMethodUrlRegister) addInstructions( newRequestBuilderIndex, """ move-object v$freeRegister, p1 - invoke-static { v$urlRegister, v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->fetchStreams(Ljava/lang/String;Ljava/util/Map;)V + invoke-static { v$buildRequestMethodUrlRegister, v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->fetchStreams(Ljava/lang/String;Ljava/util/Map;)V """ ) } @@ -187,6 +194,21 @@ fun spoofVideoStreamsPatch( // endregion + // region block getAtt request + + buildRequestMethod.apply { + val insertIndex = indexOfNewUrlRequestBuilderInstruction(this) + + addInstructions( + insertIndex, """ + invoke-static { v$buildRequestMethodUrlRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetAttRequest(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$buildRequestMethodUrlRegister + """ + ) + } + + // endregion + // region Remove /videoplayback request body to fix playback. // It is assumed, YouTube makes a request with a body tuned for Android. // Requesting streams intended for other platforms with a body tuned for Android could be the cause of 400 errors. @@ -243,6 +265,50 @@ fun spoofVideoStreamsPatch( // endregion + // region Disable SABR playback. + // If SABR is disabled, it seems 'MediaFetchHotConfig' may no longer need an override (not confirmed). + + val (mediaFetchEnumClass, sabrFieldReference) = with(mediaFetchEnumConstructorFingerprint.method) { + val stringIndex = mediaFetchEnumConstructorFingerprint.stringMatches!!.first { + it.string == DISABLED_BY_SABR_STREAMING_URI_STRING + }.index + + val mediaFetchEnumClass = definingClass + val sabrFieldIndex = indexOfFirstInstructionOrThrow(stringIndex) { + opcode == Opcode.SPUT_OBJECT && + getReference()?.type == mediaFetchEnumClass + } + + Pair( + mediaFetchEnumClass, + getInstruction(sabrFieldIndex).reference + ) + } + + fingerprint { + returns(mediaFetchEnumClass) + opcodes( + Opcode.SGET_OBJECT, + Opcode.RETURN_OBJECT, + ) + custom { method, _ -> + !method.parameterTypes.isEmpty() + } + }.method.addInstructionsWithLabels( + 0, + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->disableSABR()Z + move-result v0 + if-eqz v0, :ignore + sget-object v0, $sabrFieldReference + return-object v0 + :ignore + nop + """ + ) + + // endregion + // region turn off stream config replacement feature flag. if (fixMediaFetchHotConfigChanges()) { @@ -271,3 +337,12 @@ fun spoofVideoStreamsPatch( executeBlock() } } + +internal fun indexOfNewUrlRequestBuilderInstruction(method: Method) = method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && getReference().toString() == + "Lorg/chromium/net/CronetEngine;" + + "->newUrlRequestBuilder(" + + "Ljava/lang/String;Lorg/chromium/net/UrlRequest${'$'}Callback;" + + "Ljava/util/concurrent/Executor;" + + ")Lorg/chromium/net/UrlRequest${'$'}Builder;" +}