refactor(Spoof streaming data): Instead of replacing the entire array StreamingData.formats, replace only the approxDurationMs field

This commit is contained in:
inotia00 2024-12-20 17:35:29 +09:00
parent 3c8e61c850
commit a040b78927
7 changed files with 159 additions and 95 deletions

View File

@ -9,9 +9,10 @@ import com.google.protos.youtube.api.innertube.StreamingDataOuterClass$Streaming
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import app.revanced.extension.shared.patches.BlockRequestPatch; import app.revanced.extension.shared.patches.BlockRequestPatch;
import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest; import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest;
@ -29,10 +30,18 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_SYNC_VIDEO_LENGTH.get(); SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_SYNC_VIDEO_LENGTH.get();
/** /**
* Key: videoId. * Key: video id
* Value: original [streamingData.formats]. * Value: original video length [streamingData.formats.approxDurationMs]
*/ */
private static final ConcurrentHashMap<String, List<?>> formatsMap = new ConcurrentHashMap<>(20, 0.8f); private static final Map<String, Long> approxDurationMsMap = Collections.synchronizedMap(
new LinkedHashMap<>(100) {
private static final int CACHE_LIMIT = 50;
@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
}
});
/** /**
* Injection point. * Injection point.
@ -120,34 +129,26 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
* Injection point. * Injection point.
* <p> * <p>
* If spoofed [streamingData.formats] is empty, * If spoofed [streamingData.formats] is empty,
* Put the original [streamingData.formats] into the HashMap. * Put the original [streamingData.formats.approxDurationMs] into the HashMap.
* <p> * <p>
* Called after {@link #getStreamingData(String)}. * Called after {@link #getStreamingData(String)}.
*/ */
public static void setFormats(String videoId, StreamingDataOuterClass$StreamingData originalStreamingData, StreamingDataOuterClass$StreamingData spoofed) { public static void setApproxDurationMs(String videoId, String approxDurationMsFieldName,
if (SPOOF_STREAMING_DATA_SYNC_VIDEO_LENGTH && formatsIsEmpty(spoofed)) { StreamingDataOuterClass$StreamingData originalStreamingData, StreamingDataOuterClass$StreamingData spoofedStreamingData) {
formatsMap.put(videoId, getFormatsFromStreamingData(originalStreamingData)); if (SPOOF_STREAMING_DATA_SYNC_VIDEO_LENGTH) {
Logger.printDebug(() -> "New formats video id: " + videoId); if (formatsIsEmpty(spoofedStreamingData)) {
} List<?> originalFormats = getFormatsFromStreamingData(originalStreamingData);
} Long approxDurationMs = getApproxDurationMs(originalFormats, approxDurationMsFieldName);
if (approxDurationMs != null) {
private static boolean formatsIsEmpty(StreamingDataOuterClass$StreamingData streamingData) { approxDurationMsMap.put(videoId, approxDurationMs);
List<?> formats = getFormatsFromStreamingData(streamingData); Logger.printDebug(() -> "New approxDurationMs loaded, video id: " + videoId + ", video length: " + approxDurationMs);
return formats == null || formats.size() == 0; } else {
} Logger.printDebug(() -> "Ignoring as original approxDurationMs is not found, video id: " + videoId);
}
private static List<?> getFormatsFromStreamingData(StreamingDataOuterClass$StreamingData streamingData) { } else {
try { Logger.printDebug(() -> "Ignoring as spoofed formats is not empty, video id: " + videoId);
// Field e: 'formats'.
Field field = streamingData.getClass().getDeclaredField("e");
field.setAccessible(true);
if (field.get(streamingData) instanceof List<?> list) {
return list;
} }
} catch (NoSuchFieldException | IllegalAccessException ex) {
Logger.printException(() -> "Reflection error accessing formats", ex);
} }
return null;
} }
/** /**
@ -170,21 +171,21 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
* <p> * <p>
* Called after {@link #getStreamingData(String)}. * Called after {@link #getStreamingData(String)}.
*/ */
public static List<?> getOriginalFormats(String videoId, List<?> spoofedFormats) { public static long getApproxDurationMsFromOriginalResponse(String videoId, long lengthMilliseconds) {
if (SPOOF_STREAMING_DATA_SYNC_VIDEO_LENGTH) { if (SPOOF_STREAMING_DATA_SYNC_VIDEO_LENGTH) {
try { try {
if (videoId != null && !videoId.equals(MASKED_VIDEO_ID) && spoofedFormats.size() == 0) { if (videoId != null && !videoId.equals(MASKED_VIDEO_ID)) {
List<?> androidFormats = formatsMap.get(videoId); Long approxDurationMs = approxDurationMsMap.get(videoId);
if (androidFormats != null) { if (approxDurationMs != null) {
Logger.printDebug(() -> "Overriding iOS formats to original formats: " + videoId); Logger.printDebug(() -> "Replacing video length from " + lengthMilliseconds + " to " + approxDurationMs + " , videoId: " + videoId);
return androidFormats; return approxDurationMs;
} }
} }
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "getOriginalFormats failure", ex); Logger.printException(() -> "getOriginalFormats failure", ex);
} }
} }
return spoofedFormats; return lengthMilliseconds;
} }
/** /**
@ -226,4 +227,47 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
return videoFormat; return videoFormat;
} }
// Utils
private static boolean formatsIsEmpty(StreamingDataOuterClass$StreamingData streamingData) {
List<?> formats = getFormatsFromStreamingData(streamingData);
return formats == null || formats.size() == 0;
}
private static List<?> getFormatsFromStreamingData(StreamingDataOuterClass$StreamingData streamingData) {
try {
// Field e: 'formats'.
// Field name is always 'e', regardless of the client version.
Field field = streamingData.getClass().getDeclaredField("e");
field.setAccessible(true);
if (field.get(streamingData) instanceof List<?> list) {
return list;
}
} catch (NoSuchFieldException | IllegalAccessException ex) {
Logger.printException(() -> "Reflection error accessing formats", ex);
}
return null;
}
private static Long getApproxDurationMs(List<?> list, String approxDurationMsFieldName) {
try {
if (list != null) {
var iterator = list.listIterator();
if (iterator.hasNext()) {
var formats = iterator.next();
Field field = formats.getClass().getDeclaredField(approxDurationMsFieldName);
field.setAccessible(true);
if (field.get(formats) instanceof Long approxDurationMs) {
return approxDurationMs;
} else {
Logger.printDebug(() -> "Field type is null: " + approxDurationMsFieldName);
}
}
}
} catch (NoSuchFieldException | IllegalAccessException ex) {
Logger.printException(() -> "Reflection error accessing field: " + approxDurationMsFieldName, ex);
}
return null;
}
} }

View File

@ -2,8 +2,6 @@ package app.revanced.extension.shared.patches.spoof.requests;
import static app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA; import static app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA;
import android.util.Pair;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;

View File

@ -12,6 +12,7 @@ import app.revanced.patches.music.utils.settings.CategoryType
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
import app.revanced.patches.music.utils.settings.addSwitchPreference import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch import app.revanced.patches.music.utils.settings.settingsPatch
import app.revanced.patches.shared.formatStreamModelConstructorFingerprint
import app.revanced.util.fingerprint.matchOrThrow import app.revanced.util.fingerprint.matchOrThrow
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction

View File

@ -5,17 +5,6 @@ import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
/**
* On YouTube, this class is 'Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;'
* On YouTube Music, class names are obfuscated.
*/
internal val formatStreamModelConstructorFingerprint = legacyFingerprint(
name = "formatStreamModelConstructorFingerprint",
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
literals = listOf(45374643L),
)
/** /**
* YouTube Music 7.13.52 ~ * YouTube Music 7.13.52 ~
*/ */

View File

@ -35,6 +35,21 @@ private fun Method.indexOfFieldReference(string: String) = indexOfFirstInstructi
reference.toString() == string reference.toString() == string
} }
/**
* On YouTube, this class is 'Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;'
* On YouTube Music, class names are obfuscated.
*/
internal val formatStreamModelConstructorFingerprint = legacyFingerprint(
name = "formatStreamModelConstructorFingerprint",
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
opcodes = listOf(
Opcode.IGET_WIDE,
Opcode.IPUT_WIDE,
),
literals = listOf(45374643L),
)
internal val mdxPlayerDirectorSetVideoStageFingerprint = legacyFingerprint( internal val mdxPlayerDirectorSetVideoStageFingerprint = legacyFingerprint(
name = "mdxPlayerDirectorSetVideoStageFingerprint", name = "mdxPlayerDirectorSetVideoStageFingerprint",
strings = listOf("MdxDirector setVideoStage ad should be null when videoStage is not an Ad state ") strings = listOf("MdxDirector setVideoStage ad should be null when videoStage is not an Ad state ")

View File

@ -12,13 +12,13 @@ import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.shared.blockrequest.blockRequestPatch import app.revanced.patches.shared.blockrequest.blockRequestPatch
import app.revanced.patches.shared.extension.Constants.SPOOF_PATH import app.revanced.patches.shared.extension.Constants.SPOOF_PATH
import app.revanced.patches.shared.formatStreamModelConstructorFingerprint
import app.revanced.util.findInstructionIndicesReversedOrThrow import app.revanced.util.findInstructionIndicesReversedOrThrow
import app.revanced.util.fingerprint.definingClassOrThrow import app.revanced.util.fingerprint.definingClassOrThrow
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.matchOrThrow import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
@ -81,6 +81,13 @@ fun baseSpoofStreamingDataPatch(
// region Replace the streaming data. // region Replace the streaming data.
val approxDurationMsFieldName = formatStreamModelConstructorFingerprint.matchOrThrow().let {
with (it.method) {
val approxDurationMsFieldIndex = it.patternMatch!!.startIndex
(getInstruction<ReferenceInstruction>(approxDurationMsFieldIndex).reference as FieldReference).name
}
}
createStreamingDataFingerprint.matchOrThrow(createStreamingDataParentFingerprint) createStreamingDataFingerprint.matchOrThrow(createStreamingDataParentFingerprint)
.let { result -> .let { result ->
result.method.apply { result.method.apply {
@ -134,40 +141,42 @@ fun baseSpoofStreamingDataPatch(
addInstructionsWithLabels( addInstructionsWithLabels(
0, 0,
""" """
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z
move-result v0 move-result v0
if-eqz v0, :disabled if-eqz v0, :disabled
# Get video id. # Get video id.
iget-object v2, p1, $videoDetailsClass->c:Ljava/lang/String; iget-object v2, p1, $videoDetailsClass->c:Ljava/lang/String;
if-eqz v2, :disabled if-eqz v2, :disabled
# Get streaming data. # Get streaming data.
invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer; invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
move-result-object v3 move-result-object v3
if-eqz v3, :disabled
if-eqz v3, :disabled
# Parse streaming data.
sget-object v4, $playerProtoClass->a:$playerProtoClass # Parse streaming data.
invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass sget-object v4, $playerProtoClass->a:$playerProtoClass
move-result-object v5 invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass
check-cast v5, $playerProtoClass move-result-object v5
check-cast v5, $playerProtoClass
iget-object v6, v5, $getStreamingDataField
if-eqz v6, :disabled iget-object v6, v5, $getStreamingDataField
if-eqz v6, :disabled
# Get original streaming data.
iget-object v0, p0, $setStreamingDataField # Get original streaming data.
iget-object v0, p0, $setStreamingDataField
# Set spoofed streaming data.
iput-object v6, p0, $setStreamingDataField # Set spoofed streaming data.
iput-object v6, p0, $setStreamingDataField
# Set original streaming data formats.
invoke-static { v2, v0, v6 }, $EXTENSION_CLASS_DESCRIPTOR->setFormats(Ljava/lang/String;$STREAMING_DATA_INTERFACE$STREAMING_DATA_INTERFACE)V # Get video length from original streaming data and save to extension.
const-string v5, "$approxDurationMsFieldName"
:disabled invoke-static { v2, v5, v0, v6 }, $EXTENSION_CLASS_DESCRIPTOR->setApproxDurationMs(Ljava/lang/String;Ljava/lang/String;$STREAMING_DATA_INTERFACE$STREAMING_DATA_INTERFACE)V
return-void
""", :disabled
return-void
""",
) )
}, },
) )
@ -188,24 +197,21 @@ fun baseSpoofStreamingDataPatch(
getInstruction<TwoRegisterInstruction>(videoIdIndex).registerB getInstruction<TwoRegisterInstruction>(videoIdIndex).registerB
val videoIdReference = val videoIdReference =
getInstruction<ReferenceInstruction>(videoIdIndex).reference getInstruction<ReferenceInstruction>(videoIdIndex).reference
val formatsIndex = indexOfFirstInstructionReversedOrThrow(videoIdIndex) {
opcode == Opcode.IGET_OBJECT &&
getReference<FieldReference>()?.definingClass == STREAMING_DATA_INTERFACE
}
val freeRegister = getInstruction<OneRegisterInstruction>(
indexOfFirstInstructionOrThrow(formatsIndex, Opcode.CONST_WIDE)
).registerA
val audioCodecListRegister = getInstruction<TwoRegisterInstruction>(formatsIndex).registerA val toMillisIndex = indexOfToMillisInstruction(this)
val freeRegister =
getInstruction<FiveRegisterInstruction>(toMillisIndex).registerC
val lengthMillisecondsRegister =
getInstruction<OneRegisterInstruction>(toMillisIndex + 1).registerA
addInstructions( addInstructions(
formatsIndex + 1, """ toMillisIndex + 2, """
# Get video id. # Get video id.
iget-object v$freeRegister, v$definingClassRegister, $videoIdReference iget-object v$freeRegister, v$definingClassRegister, $videoIdReference
# Override streaming data formats. # Override streaming data formats.
invoke-static { v$freeRegister, v$audioCodecListRegister }, $EXTENSION_CLASS_DESCRIPTOR->getOriginalFormats(Ljava/lang/String;Ljava/util/List;)Ljava/util/List; invoke-static { v$freeRegister, v$lengthMillisecondsRegister, v${lengthMillisecondsRegister + 1} }, $EXTENSION_CLASS_DESCRIPTOR->getApproxDurationMsFromOriginalResponse(Ljava/lang/String;J)J
move-result-object v$audioCodecListRegister move-result-wide v$lengthMillisecondsRegister
""" """
) )
} }

View File

@ -108,7 +108,8 @@ internal val videoStreamingDataConstructorFingerprint = legacyFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
returnType = "V", returnType = "V",
customFingerprint = { method, _ -> customFingerprint = { method, _ ->
indexOfFormatStreamModelInitInstruction(method) >= 0 indexOfFormatStreamModelInitInstruction(method) >= 0 &&
indexOfToMillisInstruction(method) >= 0
}, },
) )
@ -120,6 +121,17 @@ internal fun indexOfFormatStreamModelInitInstruction(method: Method) =
reference.parameterTypes.size > 1 reference.parameterTypes.size > 1
} }
internal fun indexOfToMillisInstruction(method: Method) =
method.indexOfFirstInstruction {
val reference = getReference<MethodReference>()
opcode == Opcode.INVOKE_VIRTUAL &&
reference?.name == "toMillis"
}
/**
* On YouTube, this class is 'Lcom/google/android/libraries/youtube/innertube/model/media/VideoStreamingData;'
* On YouTube Music, class names are obfuscated.
*/
internal val videoStreamingDataToStringFingerprint = legacyFingerprint( internal val videoStreamingDataToStringFingerprint = legacyFingerprint(
name = "videoStreamingDataToStringFingerprint", name = "videoStreamingDataToStringFingerprint",
returnType = "Ljava/lang/String;", returnType = "Ljava/lang/String;",
@ -135,7 +147,6 @@ internal const val HLS_CURRENT_TIME_FEATURE_FLAG = 45355374L
internal val hlsCurrentTimeFingerprint = legacyFingerprint( internal val hlsCurrentTimeFingerprint = legacyFingerprint(
name = "hlsCurrentTimeFingerprint", name = "hlsCurrentTimeFingerprint",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = listOf("Z", "L"), parameters = listOf("Z", "L"),
literals = listOf(HLS_CURRENT_TIME_FEATURE_FLAG), literals = listOf(HLS_CURRENT_TIME_FEATURE_FLAG),
) )