Skip to content

Commit

Permalink
refactor(Spoof streaming data): Instead of replacing the entire array…
Browse files Browse the repository at this point in the history
… `StreamingData.formats`, replace only the `approxDurationMs` field
inotia00 committed Dec 20, 2024
1 parent 3c8e61c commit a040b78
Showing 7 changed files with 159 additions and 95 deletions.
Original file line number Diff line number Diff line change
@@ -9,9 +9,10 @@

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 static ByteBuffer getStreamingData(String videoId) {
* 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 @@ private static List<?> getFormatsFromStreamingData(StreamingDataOuterClass$Strea
* <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 static String appendSpoofedClient(String 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;
}
}
Original file line number Diff line number Diff line change
@@ -2,8 +2,6 @@

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;

Original file line number Diff line number Diff line change
@@ -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

Original file line number Diff line number Diff line change
@@ -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 ~
*/
Original file line number Diff line number Diff line change
@@ -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 ")
Original file line number Diff line number Diff line change
@@ -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
# 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
# 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
iget-object v6, v5, $getStreamingDataField
if-eqz v6, :disabled
# Get original streaming data.
iget-object v0, 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
:disabled
return-void
""",
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 streaming data.
invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
move-result-object v3
if-eqz v3, :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
iget-object v6, v5, $getStreamingDataField
if-eqz v6, :disabled
# Get original streaming data.
iget-object v0, p0, $setStreamingDataField
# Set spoofed streaming data.
iput-object v6, p0, $setStreamingDataField
# 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
"""
)
}
Original file line number Diff line number Diff line change
@@ -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),
)

0 comments on commit a040b78

Please sign in to comment.