refactor(Spoof streaming data): No longer using Java reflection to improve performance (#113)

* refactor(Spoof Streaming Data): Move the parser to bytecode

* Add comment

* Increase cache limit

* Fix typo

* Revert changes

* chore: Simplify

* chore: Simplify

---------

Co-authored-by: inotia00 <108592928+inotia00@users.noreply.github.com>
This commit is contained in:
Hoàng Gia Bảo 2024-12-22 13:40:37 +07:00 committed by GitHub
parent ab7f130b73
commit 92a317e312
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 143 additions and 157 deletions

View File

@ -5,13 +5,9 @@ import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.google.protos.youtube.api.innertube.StreamingDataOuterClass$StreamingData;
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 app.revanced.extension.shared.patches.BlockRequestPatch;
@ -22,12 +18,6 @@ import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class SpoofStreamingDataPatch extends BlockRequestPatch {
/**
* Even if the default client is not iOS, videos that cannot be played on Android VR or Android TV will fall back to iOS.
* Do not add a dependency that checks whether the default client is iOS or not.
*/
private static final boolean SPOOF_STREAMING_DATA_SYNC_VIDEO_LENGTH =
SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_SYNC_VIDEO_LENGTH.get();
/**
* Key: video id
@ -133,29 +123,13 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
* <p>
* Called after {@link #getStreamingData(String)}.
*/
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);
}
public static void setApproxDurationMs(String videoId, long approxDurationMs) {
if (approxDurationMs != Long.MAX_VALUE) {
approxDurationMsMap.put(videoId, approxDurationMs);
Logger.printDebug(() -> "New approxDurationMs loaded, video id: " + videoId + ", video length: " + approxDurationMs);
}
}
/**
* Looks like the initial value for the videoId field.
*/
private static final String MASKED_VIDEO_ID = "zzzzzzzzzzz";
/**
* Injection point.
* <p>
@ -171,22 +145,16 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
* <p>
* Called after {@link #getStreamingData(String)}.
*/
public static long getApproxDurationMsFromOriginalResponse(String videoId, long lengthMilliseconds) {
if (SPOOF_STREAMING_DATA_SYNC_VIDEO_LENGTH) {
try {
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);
approxDurationMsMap.remove(videoId);
return approxDurationMs;
}
}
} catch (Exception ex) {
Logger.printException(() -> "getOriginalFormats failure", ex);
public static long getApproxDurationMs(String videoId) {
if (videoId != null) {
final Long approxDurationMs = approxDurationMsMap.get(videoId);
if (approxDurationMs != null) {
Logger.printDebug(() -> "Replacing video length: " + approxDurationMs + " for videoId: " + videoId);
approxDurationMsMap.remove(videoId);
return approxDurationMs;
}
}
return lengthMilliseconds;
return Long.MAX_VALUE;
}
/**
@ -228,47 +196,4 @@ 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;
}
}

View File

@ -64,10 +64,7 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
private void updateUI() {
final ClientType clientType = Settings.SPOOF_STREAMING_DATA_TYPE.get();
final String summaryTextKey = clientType == ClientType.IOS &&
!Settings.SPOOF_STREAMING_DATA_SYNC_VIDEO_LENGTH.get()
? "revanced_spoof_streaming_data_side_effects_ios_skip_sync_video_length"
: "revanced_spoof_streaming_data_side_effects_" + clientType.name().toLowerCase();
final String summaryTextKey = "revanced_spoof_streaming_data_side_effects_" + clientType.name().toLowerCase();
setSummary(str(summaryTextKey));
setEnabled(Settings.SPOOF_STREAMING_DATA.get());

View File

@ -4,6 +4,7 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
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.removeInstruction
import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.BytecodePatchBuilder
@ -22,7 +23,7 @@ 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.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
@ -37,10 +38,6 @@ import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
const val EXTENSION_CLASS_DESCRIPTOR =
"$SPOOF_PATH/SpoofStreamingDataPatch;"
// In YouTube 17.34.36, this class is obfuscated.
const val STREAMING_DATA_INTERFACE =
"Lcom/google/protos/youtube/api/innertube/StreamingDataOuterClass${'$'}StreamingData;"
fun baseSpoofStreamingDataPatch(
block: BytecodePatchBuilder.() -> Unit = {},
executeBlock: BytecodePatchContext.() -> Unit = {},
@ -84,18 +81,51 @@ 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
val approxDurationMsReference = formatStreamModelConstructorFingerprint.matchOrThrow().let {
with (it.method) {
getInstruction<ReferenceInstruction>(it.patternMatch!!.startIndex).reference
}
}
val streamingDataFormatsReference = with(videoStreamingDataConstructorFingerprint.methodOrThrow(videoStreamingDataToStringFingerprint)) {
val getFormatsFieldIndex = indexOfGetFormatsFieldInstruction(this)
val longMaxValueIndex = indexOfLongMaxValueInstruction(this, getFormatsFieldIndex)
val longMaxValueRegister = getInstruction<OneRegisterInstruction>(longMaxValueIndex).registerA
val videoIdIndex =
indexOfFirstInstructionOrThrow(longMaxValueIndex) {
val reference = getReference<FieldReference>()
opcode == Opcode.IGET_OBJECT &&
reference?.type == "Ljava/lang/String;" &&
reference.definingClass == definingClass
}
val definingClassRegister =
getInstruction<TwoRegisterInstruction>(videoIdIndex).registerB
val videoIdReference =
getInstruction<ReferenceInstruction>(videoIdIndex).reference
addInstructions(
longMaxValueIndex + 1, """
# Get video id.
iget-object v$longMaxValueRegister, v$definingClassRegister, $videoIdReference
# Override approxDurationMs.
invoke-static { v$longMaxValueRegister }, $EXTENSION_CLASS_DESCRIPTOR->getApproxDurationMs(Ljava/lang/String;)J
move-result-wide v$longMaxValueRegister
"""
)
removeInstruction(longMaxValueIndex)
getInstruction<ReferenceInstruction>(getFormatsFieldIndex).reference
}
createStreamingDataFingerprint.matchOrThrow(createStreamingDataParentFingerprint)
.let { result ->
result.method.apply {
val setStreamDataMethodName = "patch_setStreamingData"
val resultMethodType = result.classDef.type
val calcApproxDurationMsMethodName = "patch_calcApproxDurationMs"
val resultClassDef = result.classDef
val resultMethodType = resultClassDef.type
val setStreamingDataIndex = result.patternMatch!!.startIndex
val setStreamingDataField =
getInstruction(setStreamingDataIndex).getReference<FieldReference>()
@ -124,7 +154,7 @@ fun baseSpoofStreamingDataPatch(
"$resultMethodType->$setStreamDataMethodName($videoDetailsClass)V",
)
result.classDef.methods.add(
resultClassDef.methods.add(
ImmutableMethod(
resultMethodType,
setStreamDataMethodName,
@ -167,58 +197,83 @@ fun baseSpoofStreamingDataPatch(
iget-object v6, v5, $getStreamingDataField
if-eqz v6, :disabled
# Get original streaming data.
iget-object v0, p0, $setStreamingDataField
# Caculate approxDurationMs.
invoke-direct { p0, v2 }, $resultMethodType->$calcApproxDurationMsMethodName(Ljava/lang/String;)V
# 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
""",
)
},
)
resultClassDef.methods.add(
ImmutableMethod(
resultMethodType,
calcApproxDurationMsMethodName,
listOf(
ImmutableMethodParameter(
"Ljava/lang/String;",
annotations,
"videoId"
)
),
"V",
AccessFlags.PRIVATE.value or AccessFlags.FINAL.value,
annotations,
null,
MutableMethodImplementation(12),
).toMutable().apply {
addInstructionsWithLabels(
0,
"""
# Get video format list.
iget-object v0, p0, $setStreamingDataField
iget-object v0, v0, $streamingDataFormatsReference
invoke-interface {v0}, Ljava/util/List;->iterator()Ljava/util/Iterator;
move-result-object v0
# Initialize approxDurationMs field.
const-wide v1, 0x7fffffffffffffffL
:loop
# Loop over all video formats to get the approxDurationMs
invoke-interface {v0}, Ljava/util/Iterator;->hasNext()Z
move-result v3
const-wide/16 v4, 0x0
if-eqz v3, :exit
invoke-interface {v0}, Ljava/util/Iterator;->next()Ljava/lang/Object;
move-result-object v3
check-cast v3, ${(approxDurationMsReference as FieldReference).definingClass}
# Get approxDurationMs from format
iget-wide v6, v3, $approxDurationMsReference
# Compare with zero to make sure approxDurationMs is not negative
cmp-long v8, v6, v4
if-lez v8, :loop
# Only use the min value of approxDurationMs
invoke-static {v1, v2, v6, v7}, Ljava/lang/Math;->min(JJ)J
move-result-wide v1
goto :loop
:exit
# Save approxDurationMs to integrations
invoke-static { p1, v1, v2 }, $EXTENSION_CLASS_DESCRIPTOR->setApproxDurationMs(Ljava/lang/String;J)V
return-void
""",
)
},
)
}
}
videoStreamingDataConstructorFingerprint.methodOrThrow(videoStreamingDataToStringFingerprint)
.apply {
val formatStreamModelInitIndex = indexOfFormatStreamModelInitInstruction(this)
val videoIdIndex =
indexOfFirstInstructionReversedOrThrow(formatStreamModelInitIndex) {
val reference = getReference<FieldReference>()
opcode == Opcode.IGET_OBJECT &&
reference?.type == "Ljava/lang/String;" &&
reference.definingClass == definingClass
}
val definingClassRegister =
getInstruction<TwoRegisterInstruction>(videoIdIndex).registerB
val videoIdReference =
getInstruction<ReferenceInstruction>(videoIdIndex).reference
val toMillisIndex = indexOfToMillisInstruction(this)
val freeRegister =
getInstruction<FiveRegisterInstruction>(toMillisIndex).registerC
val lengthMillisecondsRegister =
getInstruction<OneRegisterInstruction>(toMillisIndex + 1).registerA
addInstructions(
toMillisIndex + 2, """
# Get video id.
iget-object v$freeRegister, v$definingClassRegister, $videoIdReference
# Override streaming data formats.
invoke-static { v$freeRegister, v$lengthMillisecondsRegister, v${lengthMillisecondsRegister + 1} }, $EXTENSION_CLASS_DESCRIPTOR->getApproxDurationMsFromOriginalResponse(Ljava/lang/String;J)J
move-result-wide v$lengthMillisecondsRegister
"""
)
}
// endregion
// region Remove /videoplayback request body to fix playback.

View File

@ -7,8 +7,14 @@ import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
// In YouTube 17.34.36, this class is obfuscated.
const val STREAMING_DATA_INTERFACE =
"Lcom/google/protos/youtube/api/innertube/StreamingDataOuterClass${'$'}StreamingData;"
internal val buildMediaDataSourceFingerprint = legacyFingerprint(
name = "buildMediaDataSourceFingerprint",
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
@ -108,11 +114,28 @@ internal val videoStreamingDataConstructorFingerprint = legacyFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
returnType = "V",
customFingerprint = { method, _ ->
indexOfFormatStreamModelInitInstruction(method) >= 0 &&
indexOfToMillisInstruction(method) >= 0
indexOfGetFormatsFieldInstruction(method) >= 0 &&
indexOfLongMaxValueInstruction(method) >= 0 &&
indexOfFormatStreamModelInitInstruction(method) >= 0
},
)
internal fun indexOfGetFormatsFieldInstruction(method: Method) =
method.indexOfFirstInstruction {
val reference = getReference<FieldReference>()
opcode == Opcode.IGET_OBJECT &&
reference?.definingClass == STREAMING_DATA_INTERFACE &&
// Field e: 'formats'.
// Field name is always 'e', regardless of the client version.
reference.name == "e" &&
reference.type.startsWith("L")
}
internal fun indexOfLongMaxValueInstruction(method: Method, index: Int = 0) =
method.indexOfFirstInstruction(index) {
(this as? WideLiteralInstruction)?.wideLiteral == Long.MAX_VALUE
}
internal fun indexOfFormatStreamModelInitInstruction(method: Method) =
method.indexOfFirstInstruction {
val reference = getReference<MethodReference>()
@ -121,13 +144,6 @@ 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.

View File

@ -1918,16 +1918,10 @@ Tap the continue button and allow optimization changes."</string>
<string name="revanced_spoof_streaming_data_type_entry_android_vr">Android VR</string>
<string name="revanced_spoof_streaming_data_side_effects_title">Spoofing side effects</string>
<string name="revanced_spoof_streaming_data_side_effects_ios">• Not yet found.</string>
<string name="revanced_spoof_streaming_data_side_effects_ios_skip_sync_video_length">• Videos may end 1 second early.</string>
<string name="revanced_spoof_streaming_data_side_effects_android_unplugged">"• Audio track menu is missing.
• Stable volume is not available."</string>
<string name="revanced_spoof_streaming_data_side_effects_android_vr">"• Audio track menu is missing.
• Stable volume is not available."</string>
<string name="revanced_spoof_streaming_data_sync_video_length_title">Sync video length before playback</string>
<string name="revanced_spoof_streaming_data_sync_video_length_summary_on">"Video length is synced before playback.
Video length is exact value."</string>
<string name="revanced_spoof_streaming_data_sync_video_length_summary_off">"Video length is not synced before playback.
Video length may be a rounded value."</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_title">Show in Stats for nerds</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_on">Client used to fetch streaming data is shown in Stats for nerds.</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Client used to fetch streaming data is hidden in Stats for nerds.</string>

View File

@ -793,7 +793,6 @@
<SwitchPreference android:title="@string/revanced_spoof_streaming_data_title" android:key="revanced_spoof_streaming_data" android:summaryOn="@string/revanced_spoof_streaming_data_summary_on" android:summaryOff="@string/revanced_spoof_streaming_data_summary_off" />
<ListPreference android:entries="@array/revanced_spoof_streaming_data_type_entries" android:title="@string/revanced_spoof_streaming_data_type_title" android:key="revanced_spoof_streaming_data_type" android:entryValues="@array/revanced_spoof_streaming_data_type_entry_values" android:dependency="revanced_spoof_streaming_data" />
<app.revanced.extension.youtube.settings.preference.SpoofStreamingDataSideEffectsPreference android:title="@string/revanced_spoof_streaming_data_side_effects_title" />
<SwitchPreference android:title="@string/revanced_spoof_streaming_data_sync_video_length_title" android:key="revanced_spoof_streaming_data_sync_video_length" android:summaryOn="@string/revanced_spoof_streaming_data_sync_video_length_summary_on" android:summaryOff="@string/revanced_spoof_streaming_data_sync_video_length_summary_off" android:dependency="revanced_spoof_streaming_data" />
<SwitchPreference android:title="@string/revanced_spoof_streaming_data_stats_for_nerds_title" android:key="revanced_spoof_streaming_data_stats_for_nerds" android:summaryOn="@string/revanced_spoof_streaming_data_stats_for_nerds_summary_on" android:summaryOff="@string/revanced_spoof_streaming_data_stats_for_nerds_summary_off" android:dependency="revanced_spoof_streaming_data" />
</PreferenceScreen>SETTINGS: SPOOF_STREAMING_DATA -->