mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-05-28 12:50:19 +02:00
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:
parent
ab7f130b73
commit
92a317e312
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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 -->
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user