From e0b6d33df51793a39ef4e61cad74b207cb5e2d82 Mon Sep 17 00:00:00 2001 From: inotia00 <108592928+inotia00@users.noreply.github.com> Date: Tue, 17 Dec 2024 13:57:19 +0900 Subject: [PATCH] fix(YouTube - Spoof streaming data): Videos end 1 second early on iOS client --- .../spoof/SpoofStreamingDataPatch.java | 81 ++++++++++++++++++- .../spoof/requests/StreamingDataRequest.java | 10 ++- .../model/media/FormatStreamModel.java | 4 + ...StreamingDataOuterClass$StreamingData.java | 4 + .../BaseSpoofStreamingDataPatch.kt | 40 ++++++++- .../spoof/streamingdata/Fingerprints.kt | 28 +++++++ .../youtube/settings/host/values/strings.xml | 2 +- 7 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 extensions/shared/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/FormatStreamModel.java create mode 100644 extensions/shared/stub/src/main/java/com/google/protos/youtube/api/innertube/StreamingDataOuterClass$StreamingData.java 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 b3294aac3..c43bbad5b 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,10 +5,16 @@ import android.text.TextUtils; import androidx.annotation.Nullable; +import com.google.android.libraries.youtube.innertube.model.media.FormatStreamModel; +import com.google.protos.youtube.api.innertube.StreamingDataOuterClass$StreamingData; + import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Map; import app.revanced.extension.shared.patches.BlockRequestPatch; +import app.revanced.extension.shared.patches.client.AppClient.ClientType; import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.settings.BaseSettings; @@ -17,6 +23,34 @@ import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest @SuppressWarnings("unused") public class SpoofStreamingDataPatch extends BlockRequestPatch { + /** + * key: videoId + * value: android StreamingData + */ + private static final Map streamingDataMap = Collections.synchronizedMap( + new LinkedHashMap<>(10) { + private static final int CACHE_LIMIT = 5; + + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit. + } + }); + + /** + * key: android StreamingData + * value: fetched ClientType + */ + private static final Map clientTypeMap = Collections.synchronizedMap( + new LinkedHashMap<>(10) { + private static final int CACHE_LIMIT = 5; + + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit. + } + }); + /** * Injection point. */ @@ -66,9 +100,11 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch { * Injection point. * Fix playback by replace the streaming data. * Called after {@link #fetchStreams(String, Map)}. + * + * @param originalStreamingData Original StreamingData. */ @Nullable - public static ByteBuffer getStreamingData(String videoId) { + public static ByteBuffer getStreamingData(String videoId, StreamingDataOuterClass$StreamingData originalStreamingData) { if (SPOOF_STREAMING_DATA) { try { StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId); @@ -85,7 +121,11 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch { var stream = request.getStream(); if (stream != null) { Logger.printDebug(() -> "Overriding video stream: " + videoId); - return stream; + // Put the videoId, originalStreamingData, and the clientType used for spoofing into a HashMap. + streamingDataMap.put(videoId, originalStreamingData); + clientTypeMap.put(originalStreamingData, stream.second); + + return stream.first; } } @@ -100,7 +140,42 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch { /** * Injection point. - * Called after {@link #getStreamingData(String)}. + *

+ * It seems that some 'adaptiveFormats' are missing from the initial response of streaming data on iOS. + * Since the {@link FormatStreamModel} class for measuring the video length is not initialized on iOS clients, + * The video length field is always initialized to an estimated value, not the actual value. + *

+ * To fix this, replace streamingData (spoofedStreamingData) with originalStreamingData, which is only used to initialize the {@link FormatStreamModel} class to measure the video length. + *

+ * Called after {@link #getStreamingData(String, StreamingDataOuterClass$StreamingData)}. + * + * @param spoofedStreamingData Spoofed StreamingData. + */ + public static StreamingDataOuterClass$StreamingData getOriginalStreamingData(String videoId, StreamingDataOuterClass$StreamingData spoofedStreamingData) { + if (SPOOF_STREAMING_DATA) { + try { + StreamingDataOuterClass$StreamingData androidStreamingData = streamingDataMap.get(videoId); + if (androidStreamingData != null) { + ClientType clientType = clientTypeMap.get(androidStreamingData); + if (clientType == ClientType.IOS) { + Logger.printDebug(() -> "Overriding iOS streaming data to original streaming data: " + videoId); + return androidStreamingData; + } else { + Logger.printDebug(() -> "Not overriding original streaming data as spoofed client is not iOS: " + videoId + " (" + clientType + ")"); + } + } else { + Logger.printDebug(() -> "Not overriding original streaming data (original streaming data is null): " + videoId); + } + } catch (Exception ex) { + Logger.printException(() -> "getOriginalStreamingData failure", ex); + } + } + return spoofedStreamingData; + } + + /** + * Injection point. + * Called after {@link #getStreamingData(String, StreamingDataOuterClass$StreamingData)}. */ @Nullable public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) { diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java index 4cd28fa7f..e5944d9e6 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java @@ -2,6 +2,8 @@ package app.revanced.extension.shared.patches.spoof.requests; 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; @@ -93,7 +95,7 @@ public class StreamingDataRequest { } private final String videoId; - private final Future future; + private final Future> future; private StreamingDataRequest(String videoId, Map playerHeaders) { Objects.requireNonNull(playerHeaders); @@ -170,7 +172,7 @@ public class StreamingDataRequest { return null; } - private static ByteBuffer fetch(String videoId, Map playerHeaders) { + private static Pair fetch(String videoId, Map playerHeaders) { lastSpoofedClientType = null; // Retry with different client if empty response body is received. @@ -193,7 +195,7 @@ public class StreamingDataRequest { } lastSpoofedClientType = clientType; - return ByteBuffer.wrap(baos.toByteArray()); + return new Pair<>(ByteBuffer.wrap(baos.toByteArray()), clientType); } } } catch (IOException ex) { @@ -211,7 +213,7 @@ public class StreamingDataRequest { } @Nullable - public ByteBuffer getStream() { + public Pair getStream() { try { return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS); } catch (TimeoutException ex) { diff --git a/extensions/shared/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/FormatStreamModel.java b/extensions/shared/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/FormatStreamModel.java new file mode 100644 index 000000000..5c0df267f --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/FormatStreamModel.java @@ -0,0 +1,4 @@ +package com.google.android.libraries.youtube.innertube.model.media; + +public class FormatStreamModel { +} \ No newline at end of file diff --git a/extensions/shared/stub/src/main/java/com/google/protos/youtube/api/innertube/StreamingDataOuterClass$StreamingData.java b/extensions/shared/stub/src/main/java/com/google/protos/youtube/api/innertube/StreamingDataOuterClass$StreamingData.java new file mode 100644 index 000000000..cb08ae162 --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/google/protos/youtube/api/innertube/StreamingDataOuterClass$StreamingData.java @@ -0,0 +1,4 @@ +package com.google.protos.youtube.api.innertube; + +public class StreamingDataOuterClass$StreamingData { +} \ No newline at end of file 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 ffb58d9e7..ea19354b6 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 @@ -34,6 +34,10 @@ 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 = {}, @@ -130,7 +134,8 @@ fun baseSpoofStreamingDataPatch( if-eqz v2, :disabled # Get streaming data. - invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer; + iget-object v6, p0, $setStreamingDataField + invoke-static { v2, v6 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;$STREAMING_DATA_INTERFACE)Ljava/nio/ByteBuffer; move-result-object v3 if-eqz v3, :disabled @@ -154,6 +159,39 @@ fun baseSpoofStreamingDataPatch( } } + videoStreamingDataConstructorFingerprint.methodOrThrow(videoStreamingDataToStringFingerprint).apply { + val formatStreamModelInitIndex = indexOfFormatStreamModelInitInstruction(this) + val getVideoIdIndex = indexOfFirstInstructionReversedOrThrow(formatStreamModelInitIndex) { + val reference = getReference() + opcode == Opcode.IGET_OBJECT && + reference?.type == "Ljava/lang/String;" && + reference.definingClass == definingClass + } + val getVideoIdReference = getInstruction(getVideoIdIndex).reference + val insertIndex = indexOfFirstInstructionReversedOrThrow(getVideoIdIndex) { + opcode == Opcode.IGET_OBJECT && + getReference()?.definingClass == STREAMING_DATA_INTERFACE + } + + val (freeRegister, streamingDataRegister) = with(getInstruction(insertIndex)) { + Pair(registerA, registerB) + } + val definingClassRegister = getInstruction(getVideoIdIndex).registerB + val insertReference = getInstruction(insertIndex).reference + + replaceInstruction( + insertIndex, + "iget-object v$freeRegister, v$freeRegister, $insertReference" + ) + addInstructions( + insertIndex, """ + iget-object v$freeRegister, v$definingClassRegister, $getVideoIdReference + invoke-static { v$freeRegister, v$streamingDataRegister }, $EXTENSION_CLASS_DESCRIPTOR->getOriginalStreamingData(Ljava/lang/String;$STREAMING_DATA_INTERFACE)$STREAMING_DATA_INTERFACE + move-result-object v$freeRegister + """ + ) + } + // 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 caf2d88fd..c685b9d35 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 @@ -103,6 +103,34 @@ internal val protobufClassParseByteBufferFingerprint = legacyFingerprint( customFingerprint = { method, _ -> method.name == "parseFrom" }, ) +internal val videoStreamingDataConstructorFingerprint = legacyFingerprint( + name = "videoStreamingDataConstructorFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + returnType = "V", + customFingerprint = { method, _ -> + indexOfFormatStreamModelInitInstruction(method) >= 0 + }, +) + +internal fun indexOfFormatStreamModelInitInstruction(method: Method) = + method.indexOfFirstInstruction { + val reference = getReference() + opcode == Opcode.INVOKE_DIRECT && + reference?.name == "" && + reference.parameterTypes.size > 1 + } + +internal val videoStreamingDataToStringFingerprint = legacyFingerprint( + name = "videoStreamingDataToStringFingerprint", + returnType = "Ljava/lang/String;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + strings = listOf("VideoStreamingData(itags="), + customFingerprint = { method, _ -> + method.name == "toString" + }, +) + internal const val HLS_CURRENT_TIME_FEATURE_FLAG = 45355374L internal val hlsCurrentTimeFingerprint = legacyFingerprint( 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 299838d59..6058923ae 100644 --- a/patches/src/main/resources/youtube/settings/host/values/strings.xml +++ b/patches/src/main/resources/youtube/settings/host/values/strings.xml @@ -1905,7 +1905,7 @@ Tap on the continue button and disable battery optimizations." Android TV Android VR Spoofing side effects - • Videos may end 1 second early. + • Not yet found. "• Audio track menu is missing. • Stable volume is not available." "• Audio track menu is missing.