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 androidx.annotation.Nullable;
import com.google.protos.youtube.api.innertube.StreamingDataOuterClass$StreamingData;
import java.lang.reflect.Field;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import app.revanced.extension.shared.patches.BlockRequestPatch; import app.revanced.extension.shared.patches.BlockRequestPatch;
@ -22,12 +18,6 @@ import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class SpoofStreamingDataPatch extends BlockRequestPatch { 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 * Key: video id
@ -133,29 +123,13 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
* <p> * <p>
* Called after {@link #getStreamingData(String)}. * Called after {@link #getStreamingData(String)}.
*/ */
public static void setApproxDurationMs(String videoId, String approxDurationMsFieldName, public static void setApproxDurationMs(String videoId, long approxDurationMs) {
StreamingDataOuterClass$StreamingData originalStreamingData, StreamingDataOuterClass$StreamingData spoofedStreamingData) { if (approxDurationMs != Long.MAX_VALUE) {
if (SPOOF_STREAMING_DATA_SYNC_VIDEO_LENGTH) { approxDurationMsMap.put(videoId, approxDurationMs);
if (formatsIsEmpty(spoofedStreamingData)) { Logger.printDebug(() -> "New approxDurationMs loaded, video id: " + videoId + ", video length: " + approxDurationMs);
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);
}
} }
} }
/**
* Looks like the initial value for the videoId field.
*/
private static final String MASKED_VIDEO_ID = "zzzzzzzzzzz";
/** /**
* Injection point. * Injection point.
* <p> * <p>
@ -171,22 +145,16 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
* <p> * <p>
* Called after {@link #getStreamingData(String)}. * Called after {@link #getStreamingData(String)}.
*/ */
public static long getApproxDurationMsFromOriginalResponse(String videoId, long lengthMilliseconds) { public static long getApproxDurationMs(String videoId) {
if (SPOOF_STREAMING_DATA_SYNC_VIDEO_LENGTH) { if (videoId != null) {
try { final Long approxDurationMs = approxDurationMsMap.get(videoId);
if (videoId != null && !videoId.equals(MASKED_VIDEO_ID)) { if (approxDurationMs != null) {
Long approxDurationMs = approxDurationMsMap.get(videoId); Logger.printDebug(() -> "Replacing video length: " + approxDurationMs + " for videoId: " + videoId);
if (approxDurationMs != null) { approxDurationMsMap.remove(videoId);
Logger.printDebug(() -> "Replacing video length from " + lengthMilliseconds + " to " + approxDurationMs + " , videoId: " + videoId); return approxDurationMs;
approxDurationMsMap.remove(videoId);
return approxDurationMs;
}
}
} catch (Exception ex) {
Logger.printException(() -> "getOriginalFormats failure", ex);
} }
} }
return lengthMilliseconds; return Long.MAX_VALUE;
} }
/** /**
@ -228,47 +196,4 @@ 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

@ -64,10 +64,7 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
private void updateUI() { private void updateUI() {
final ClientType clientType = Settings.SPOOF_STREAMING_DATA_TYPE.get(); final ClientType clientType = Settings.SPOOF_STREAMING_DATA_TYPE.get();
final String summaryTextKey = clientType == ClientType.IOS && final String summaryTextKey = "revanced_spoof_streaming_data_side_effects_" + clientType.name().toLowerCase();
!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();
setSummary(str(summaryTextKey)); setSummary(str(summaryTextKey));
setEnabled(Settings.SPOOF_STREAMING_DATA.get()); 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.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction 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.instructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.BytecodePatchBuilder 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.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.indexOfFirstInstructionReversedOrThrow import app.revanced.util.indexOfFirstInstructionOrThrow
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
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation 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 = const val EXTENSION_CLASS_DESCRIPTOR =
"$SPOOF_PATH/SpoofStreamingDataPatch;" "$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( fun baseSpoofStreamingDataPatch(
block: BytecodePatchBuilder.() -> Unit = {}, block: BytecodePatchBuilder.() -> Unit = {},
executeBlock: BytecodePatchContext.() -> Unit = {}, executeBlock: BytecodePatchContext.() -> Unit = {},
@ -84,18 +81,51 @@ fun baseSpoofStreamingDataPatch(
// region Replace the streaming data. // region Replace the streaming data.
val approxDurationMsFieldName = formatStreamModelConstructorFingerprint.matchOrThrow().let { val approxDurationMsReference = formatStreamModelConstructorFingerprint.matchOrThrow().let {
with(it.method) { with (it.method) {
val approxDurationMsFieldIndex = it.patternMatch!!.startIndex getInstruction<ReferenceInstruction>(it.patternMatch!!.startIndex).reference
(getInstruction<ReferenceInstruction>(approxDurationMsFieldIndex).reference as FieldReference).name
} }
} }
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) createStreamingDataFingerprint.matchOrThrow(createStreamingDataParentFingerprint)
.let { result -> .let { result ->
result.method.apply { result.method.apply {
val setStreamDataMethodName = "patch_setStreamingData" 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 setStreamingDataIndex = result.patternMatch!!.startIndex
val setStreamingDataField = val setStreamingDataField =
getInstruction(setStreamingDataIndex).getReference<FieldReference>() getInstruction(setStreamingDataIndex).getReference<FieldReference>()
@ -124,7 +154,7 @@ fun baseSpoofStreamingDataPatch(
"$resultMethodType->$setStreamDataMethodName($videoDetailsClass)V", "$resultMethodType->$setStreamDataMethodName($videoDetailsClass)V",
) )
result.classDef.methods.add( resultClassDef.methods.add(
ImmutableMethod( ImmutableMethod(
resultMethodType, resultMethodType,
setStreamDataMethodName, setStreamDataMethodName,
@ -167,58 +197,83 @@ fun baseSpoofStreamingDataPatch(
iget-object v6, v5, $getStreamingDataField iget-object v6, v5, $getStreamingDataField
if-eqz v6, :disabled if-eqz v6, :disabled
# Get original streaming data. # Caculate approxDurationMs.
iget-object v0, p0, $setStreamingDataField invoke-direct { p0, v2 }, $resultMethodType->$calcApproxDurationMsMethodName(Ljava/lang/String;)V
# Set spoofed streaming data. # Set spoofed streaming data.
iput-object v6, p0, $setStreamingDataField 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 :disabled
return-void 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 // endregion
// region Remove /videoplayback request body to fix playback. // 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.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method 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 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( internal val buildMediaDataSourceFingerprint = legacyFingerprint(
name = "buildMediaDataSourceFingerprint", name = "buildMediaDataSourceFingerprint",
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
@ -108,11 +114,28 @@ 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 && indexOfGetFormatsFieldInstruction(method) >= 0 &&
indexOfToMillisInstruction(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) = internal fun indexOfFormatStreamModelInitInstruction(method: Method) =
method.indexOfFirstInstruction { method.indexOfFirstInstruction {
val reference = getReference<MethodReference>() val reference = getReference<MethodReference>()
@ -121,13 +144,6 @@ 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, this class is 'Lcom/google/android/libraries/youtube/innertube/model/media/VideoStreamingData;'
* On YouTube Music, class names are obfuscated. * 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_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_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">• 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. <string name="revanced_spoof_streaming_data_side_effects_android_unplugged">"• Audio track menu is missing.
• Stable volume is not available."</string> • Stable volume is not available."</string>
<string name="revanced_spoof_streaming_data_side_effects_android_vr">"• Audio track menu is missing. <string name="revanced_spoof_streaming_data_side_effects_android_vr">"• Audio track menu is missing.
• Stable volume is not available."</string> • 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_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_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> <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" /> <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" /> <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" /> <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" /> <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 --> </PreferenceScreen>SETTINGS: SPOOF_STREAMING_DATA -->