diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java index 318c0d971..181d0607a 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java @@ -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 { *

* 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. *

@@ -171,22 +145,16 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch { *

* 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; - } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java index e6e27a972..92eec6bcf 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java @@ -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()); diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt index 811e1c9b6..86723635b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt @@ -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(approxDurationMsFieldIndex).reference as FieldReference).name + val approxDurationMsReference = formatStreamModelConstructorFingerprint.matchOrThrow().let { + with (it.method) { + getInstruction(it.patternMatch!!.startIndex).reference } } + val streamingDataFormatsReference = with(videoStreamingDataConstructorFingerprint.methodOrThrow(videoStreamingDataToStringFingerprint)) { + val getFormatsFieldIndex = indexOfGetFormatsFieldInstruction(this) + val longMaxValueIndex = indexOfLongMaxValueInstruction(this, getFormatsFieldIndex) + val longMaxValueRegister = getInstruction(longMaxValueIndex).registerA + val videoIdIndex = + indexOfFirstInstructionOrThrow(longMaxValueIndex) { + val reference = getReference() + opcode == Opcode.IGET_OBJECT && + reference?.type == "Ljava/lang/String;" && + reference.definingClass == definingClass + } + + val definingClassRegister = + getInstruction(videoIdIndex).registerB + val videoIdReference = + getInstruction(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(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() @@ -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() - opcode == Opcode.IGET_OBJECT && - reference?.type == "Ljava/lang/String;" && - reference.definingClass == definingClass - } - val definingClassRegister = - getInstruction(videoIdIndex).registerB - val videoIdReference = - getInstruction(videoIdIndex).reference - - val toMillisIndex = indexOfToMillisInstruction(this) - val freeRegister = - getInstruction(toMillisIndex).registerC - val lengthMillisecondsRegister = - getInstruction(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. diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt index 6b8f1937e..7a735fcb4 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt @@ -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() + 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() @@ -121,13 +144,6 @@ internal fun indexOfFormatStreamModelInitInstruction(method: Method) = reference.parameterTypes.size > 1 } -internal fun indexOfToMillisInstruction(method: Method) = - method.indexOfFirstInstruction { - val reference = getReference() - 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. diff --git a/patches/src/main/resources/youtube/settings/host/values/strings.xml b/patches/src/main/resources/youtube/settings/host/values/strings.xml index 28863227d..fa7c5f4a5 100644 --- a/patches/src/main/resources/youtube/settings/host/values/strings.xml +++ b/patches/src/main/resources/youtube/settings/host/values/strings.xml @@ -1918,16 +1918,10 @@ Tap the continue button and allow optimization changes." Android VR Spoofing side effects • Not yet found. - • Videos may end 1 second early. "• Audio track menu is missing. • Stable volume is not available." "• Audio track menu is missing. • Stable volume is not available." - Sync video length before playback - "Video length is synced before playback. -Video length is exact value." - "Video length is not synced before playback. -Video length may be a rounded value." Show in Stats for nerds Client used to fetch streaming data is shown in Stats for nerds. Client used to fetch streaming data is hidden in Stats for nerds. diff --git a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml index 0b04b9868..bda85ece1 100644 --- a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml +++ b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml @@ -793,7 +793,6 @@ - SETTINGS: SPOOF_STREAMING_DATA -->