mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-04-29 22:24:31 +02:00
refactor(Spoof streaming data): Instead of replacing the entire array StreamingData.formats
, replace only the approxDurationMs
field
This commit is contained in:
parent
3c8e61c850
commit
a040b78927
@ -9,9 +9,10 @@ import com.google.protos.youtube.api.innertube.StreamingDataOuterClass$Streaming
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import app.revanced.extension.shared.patches.BlockRequestPatch;
|
||||
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();
|
||||
|
||||
/**
|
||||
* Key: videoId.
|
||||
* Value: original [streamingData.formats].
|
||||
* Key: video id
|
||||
* 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.
|
||||
@ -120,34 +129,26 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
||||
* Injection point.
|
||||
* <p>
|
||||
* If spoofed [streamingData.formats] is empty,
|
||||
* Put the original [streamingData.formats] into the HashMap.
|
||||
* Put the original [streamingData.formats.approxDurationMs] into the HashMap.
|
||||
* <p>
|
||||
* Called after {@link #getStreamingData(String)}.
|
||||
*/
|
||||
public static void setFormats(String videoId, StreamingDataOuterClass$StreamingData originalStreamingData, StreamingDataOuterClass$StreamingData spoofed) {
|
||||
if (SPOOF_STREAMING_DATA_SYNC_VIDEO_LENGTH && formatsIsEmpty(spoofed)) {
|
||||
formatsMap.put(videoId, getFormatsFromStreamingData(originalStreamingData));
|
||||
Logger.printDebug(() -> "New formats video id: " + videoId);
|
||||
}
|
||||
}
|
||||
|
||||
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 field = streamingData.getClass().getDeclaredField("e");
|
||||
field.setAccessible(true);
|
||||
if (field.get(streamingData) instanceof List<?> list) {
|
||||
return list;
|
||||
public static void setApproxDurationMs(String videoId, String approxDurationMsFieldName,
|
||||
StreamingDataOuterClass$StreamingData originalStreamingData, StreamingDataOuterClass$StreamingData spoofedStreamingData) {
|
||||
if (SPOOF_STREAMING_DATA_SYNC_VIDEO_LENGTH) {
|
||||
if (formatsIsEmpty(spoofedStreamingData)) {
|
||||
List<?> originalFormats = getFormatsFromStreamingData(originalStreamingData);
|
||||
Long approxDurationMs = getApproxDurationMs(originalFormats, approxDurationMsFieldName);
|
||||
if (approxDurationMs != null) {
|
||||
approxDurationMsMap.put(videoId, approxDurationMs);
|
||||
Logger.printDebug(() -> "New approxDurationMs loaded, video id: " + videoId + ", video length: " + approxDurationMs);
|
||||
} else {
|
||||
Logger.printDebug(() -> "Ignoring as original approxDurationMs is not found, video id: " + videoId);
|
||||
}
|
||||
} else {
|
||||
Logger.printDebug(() -> "Ignoring as spoofed formats is not empty, video id: " + videoId);
|
||||
}
|
||||
} catch (NoSuchFieldException | IllegalAccessException ex) {
|
||||
Logger.printException(() -> "Reflection error accessing formats", ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -170,21 +171,21 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
||||
* <p>
|
||||
* 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) {
|
||||
try {
|
||||
if (videoId != null && !videoId.equals(MASKED_VIDEO_ID) && spoofedFormats.size() == 0) {
|
||||
List<?> androidFormats = formatsMap.get(videoId);
|
||||
if (androidFormats != null) {
|
||||
Logger.printDebug(() -> "Overriding iOS formats to original formats: " + videoId);
|
||||
return androidFormats;
|
||||
if (videoId != null && !videoId.equals(MASKED_VIDEO_ID)) {
|
||||
Long approxDurationMs = approxDurationMsMap.get(videoId);
|
||||
if (approxDurationMs != null) {
|
||||
Logger.printDebug(() -> "Replacing video length from " + lengthMilliseconds + " to " + approxDurationMs + " , videoId: " + videoId);
|
||||
return approxDurationMs;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "getOriginalFormats failure", ex);
|
||||
}
|
||||
}
|
||||
return spoofedFormats;
|
||||
return lengthMilliseconds;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -226,4 +227,47 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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 android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
|
@ -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.addSwitchPreference
|
||||
import app.revanced.patches.music.utils.settings.settingsPatch
|
||||
import app.revanced.patches.shared.formatStreamModelConstructorFingerprint
|
||||
import app.revanced.util.fingerprint.matchOrThrow
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
||||
|
||||
|
@ -5,17 +5,6 @@ import app.revanced.util.or
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
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 ~
|
||||
*/
|
||||
|
@ -35,6 +35,21 @@ private fun Method.indexOfFieldReference(string: String) = indexOfFirstInstructi
|
||||
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(
|
||||
name = "mdxPlayerDirectorSetVideoStageFingerprint",
|
||||
strings = listOf("MdxDirector setVideoStage ad should be null when videoStage is not an Ad state ")
|
||||
|
@ -12,13 +12,13 @@ import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||
import app.revanced.patches.shared.blockrequest.blockRequestPatch
|
||||
import app.revanced.patches.shared.extension.Constants.SPOOF_PATH
|
||||
import app.revanced.patches.shared.formatStreamModelConstructorFingerprint
|
||||
import app.revanced.util.findInstructionIndicesReversedOrThrow
|
||||
import app.revanced.util.fingerprint.definingClassOrThrow
|
||||
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
|
||||
import app.revanced.util.fingerprint.matchOrThrow
|
||||
import app.revanced.util.fingerprint.methodOrThrow
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
@ -81,6 +81,13 @@ fun baseSpoofStreamingDataPatch(
|
||||
|
||||
// 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)
|
||||
.let { result ->
|
||||
result.method.apply {
|
||||
@ -134,40 +141,42 @@ fun baseSpoofStreamingDataPatch(
|
||||
addInstructionsWithLabels(
|
||||
0,
|
||||
"""
|
||||
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z
|
||||
move-result v0
|
||||
if-eqz v0, :disabled
|
||||
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z
|
||||
move-result v0
|
||||
if-eqz v0, :disabled
|
||||
|
||||
# Get video id.
|
||||
iget-object v2, p1, $videoDetailsClass->c:Ljava/lang/String;
|
||||
if-eqz v2, :disabled
|
||||
# Get video id.
|
||||
iget-object v2, p1, $videoDetailsClass->c:Ljava/lang/String;
|
||||
if-eqz v2, :disabled
|
||||
|
||||
# Get streaming data.
|
||||
invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
|
||||
move-result-object v3
|
||||
if-eqz v3, :disabled
|
||||
# Get streaming data.
|
||||
invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
|
||||
move-result-object v3
|
||||
|
||||
# Parse streaming data.
|
||||
sget-object v4, $playerProtoClass->a:$playerProtoClass
|
||||
invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass
|
||||
move-result-object v5
|
||||
check-cast v5, $playerProtoClass
|
||||
if-eqz v3, :disabled
|
||||
|
||||
iget-object v6, v5, $getStreamingDataField
|
||||
if-eqz v6, :disabled
|
||||
# Parse streaming data.
|
||||
sget-object v4, $playerProtoClass->a:$playerProtoClass
|
||||
invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass
|
||||
move-result-object v5
|
||||
check-cast v5, $playerProtoClass
|
||||
|
||||
# Get original streaming data.
|
||||
iget-object v0, p0, $setStreamingDataField
|
||||
iget-object v6, v5, $getStreamingDataField
|
||||
if-eqz v6, :disabled
|
||||
|
||||
# Set spoofed streaming data.
|
||||
iput-object v6, p0, $setStreamingDataField
|
||||
# Get original streaming data.
|
||||
iget-object v0, 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
|
||||
# Set spoofed streaming data.
|
||||
iput-object v6, p0, $setStreamingDataField
|
||||
|
||||
:disabled
|
||||
return-void
|
||||
""",
|
||||
# Get video length from original streaming data and save to extension.
|
||||
const-string v5, "$approxDurationMsFieldName"
|
||||
invoke-static { v2, v5, v0, v6 }, $EXTENSION_CLASS_DESCRIPTOR->setApproxDurationMs(Ljava/lang/String;Ljava/lang/String;$STREAMING_DATA_INTERFACE$STREAMING_DATA_INTERFACE)V
|
||||
|
||||
:disabled
|
||||
return-void
|
||||
""",
|
||||
)
|
||||
},
|
||||
)
|
||||
@ -188,24 +197,21 @@ fun baseSpoofStreamingDataPatch(
|
||||
getInstruction<TwoRegisterInstruction>(videoIdIndex).registerB
|
||||
val videoIdReference =
|
||||
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(
|
||||
formatsIndex + 1, """
|
||||
toMillisIndex + 2, """
|
||||
# Get video id.
|
||||
iget-object v$freeRegister, v$definingClassRegister, $videoIdReference
|
||||
|
||||
# Override streaming data formats.
|
||||
invoke-static { v$freeRegister, v$audioCodecListRegister }, $EXTENSION_CLASS_DESCRIPTOR->getOriginalFormats(Ljava/lang/String;Ljava/util/List;)Ljava/util/List;
|
||||
move-result-object v$audioCodecListRegister
|
||||
invoke-static { v$freeRegister, v$lengthMillisecondsRegister, v${lengthMillisecondsRegister + 1} }, $EXTENSION_CLASS_DESCRIPTOR->getApproxDurationMsFromOriginalResponse(Ljava/lang/String;J)J
|
||||
move-result-wide v$lengthMillisecondsRegister
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
@ -108,7 +108,8 @@ internal val videoStreamingDataConstructorFingerprint = legacyFingerprint(
|
||||
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
|
||||
returnType = "V",
|
||||
customFingerprint = { method, _ ->
|
||||
indexOfFormatStreamModelInitInstruction(method) >= 0
|
||||
indexOfFormatStreamModelInitInstruction(method) >= 0 &&
|
||||
indexOfToMillisInstruction(method) >= 0
|
||||
},
|
||||
)
|
||||
|
||||
@ -120,6 +121,17 @@ internal fun indexOfFormatStreamModelInitInstruction(method: Method) =
|
||||
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(
|
||||
name = "videoStreamingDataToStringFingerprint",
|
||||
returnType = "Ljava/lang/String;",
|
||||
@ -135,7 +147,6 @@ internal const val HLS_CURRENT_TIME_FEATURE_FLAG = 45355374L
|
||||
|
||||
internal val hlsCurrentTimeFingerprint = legacyFingerprint(
|
||||
name = "hlsCurrentTimeFingerprint",
|
||||
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
|
||||
parameters = listOf("Z", "L"),
|
||||
literals = listOf(HLS_CURRENT_TIME_FEATURE_FLAG),
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user