mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-05-22 19:09:12 +02:00
refactor(Spoof streaming data): Improve hooking performance
This commit is contained in:
parent
5ffbb4714a
commit
9ed8754bb7
@ -1,22 +1,19 @@
|
||||
package app.revanced.extension.shared.patches.spoof;
|
||||
|
||||
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
|
||||
|
||||
import android.net.Uri;
|
||||
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.lang.reflect.Field;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import app.revanced.extension.shared.patches.BlockRequestPatch;
|
||||
import app.revanced.extension.shared.patches.client.AppClient.ClientType;
|
||||
import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
@ -27,17 +24,9 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
||||
|
||||
/**
|
||||
* Key: videoId.
|
||||
* Value: Original StreamingData of Android client.
|
||||
* Value: original [streamingData.formats].
|
||||
*/
|
||||
private static final Map<String, StreamingDataOuterClass$StreamingData> 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.
|
||||
}
|
||||
});
|
||||
private static final ConcurrentHashMap<String, List<?>> formatsMap = new ConcurrentHashMap<>(20, 0.8f);
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
@ -48,6 +37,7 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* This method is only invoked when playing a livestream on an iOS client.
|
||||
*/
|
||||
public static boolean fixHLSCurrentTime(boolean original) {
|
||||
if (!SPOOF_STREAMING_DATA) {
|
||||
@ -88,11 +78,9 @@ 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, StreamingDataOuterClass$StreamingData originalStreamingData) {
|
||||
public static ByteBuffer getStreamingData(String videoId) {
|
||||
if (SPOOF_STREAMING_DATA) {
|
||||
try {
|
||||
StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);
|
||||
@ -108,25 +96,8 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
||||
|
||||
var stream = request.getStream();
|
||||
if (stream != null) {
|
||||
ByteBuffer spoofedStreamingData = stream.first;
|
||||
ClientType spoofedClientType = stream.second;
|
||||
|
||||
Logger.printDebug(() -> "Overriding video stream: " + videoId);
|
||||
|
||||
// Put the videoId and originalStreamingData into a HashMap.
|
||||
if (spoofedClientType == ClientType.IOS) {
|
||||
// For YT Music 6.20.51, which is supported by RVX, it can run on Android 5.0 (SDK 21).
|
||||
// The IDE does not make any suggestions since the project's minSDK is 24, but you should check the SDK version for compatibility with SDK 21.
|
||||
if (isSDKAbove(24)) {
|
||||
streamingDataMap.putIfAbsent(videoId, originalStreamingData);
|
||||
} else {
|
||||
if (!streamingDataMap.containsKey(videoId)) {
|
||||
streamingDataMap.put(videoId, originalStreamingData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return spoofedStreamingData;
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,39 +113,77 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* In iOS Clients, Progressive Streaming are not available, so 'formats' field have been removed
|
||||
* completely from the initial response of streaming data.
|
||||
* Therefore, {@link FormatStreamModel} class is never be initialized, and the video length field
|
||||
* is set with an estimated value from `adaptiveFormats` instead.
|
||||
* If spoofed [streamingData.formats] is empty,
|
||||
* Put the original [streamingData.formats] into the HashMap.
|
||||
* <p>
|
||||
* To get workaround with this, replace streamingData (spoofedStreamingData) with originalStreamingData,
|
||||
* which is only used to initialize the {@link FormatStreamModel} class to calculate the video length.
|
||||
* The playback issues shouldn't occur since the integrity check is not applied for Progressive Stream.
|
||||
* <p>
|
||||
* Called after {@link #getStreamingData(String, StreamingDataOuterClass$StreamingData)}.
|
||||
*
|
||||
* @param spoofedStreamingData Spoofed StreamingData.
|
||||
* Called after {@link #getStreamingData(String)}.
|
||||
*/
|
||||
public static StreamingDataOuterClass$StreamingData getOriginalStreamingData(String videoId, StreamingDataOuterClass$StreamingData spoofedStreamingData) {
|
||||
public static void setFormats(String videoId, StreamingDataOuterClass$StreamingData originalStreamingData, StreamingDataOuterClass$StreamingData spoofed) {
|
||||
if (formatsIsEmpty(spoofed)) {
|
||||
formatsMap.put(videoId, getFormatsFromStreamingData(originalStreamingData));
|
||||
Logger.printDebug(() -> "New formats video id: " + videoId);
|
||||
}
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks like the initial value for the videoId field.
|
||||
*/
|
||||
private static final String MASKED_VIDEO_ID = "zzzzzzzzzzz";
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* When measuring the length of a video in an Android YouTube client,
|
||||
* the client first checks if the streaming data contains [streamingData.formats.approxDurationMs].
|
||||
* <p>
|
||||
* If the streaming data response contains [approxDurationMs] (Long type, actual value), this value will be the video length.
|
||||
* <p>
|
||||
* If [streamingData.formats] (List type) is empty, the [approxDurationMs] value cannot be accessed,
|
||||
* So it falls back to the value of [videoDetails.lengthSeconds] (Integer type, approximate value) multiplied by 1000.
|
||||
* <p>
|
||||
* For iOS clients, [streamingData.formats] (List type) is always empty, so it always falls back to the approximate value.
|
||||
* <p>
|
||||
* Called after {@link #getStreamingData(String)}.
|
||||
*/
|
||||
public static List<?> getOriginalFormats(String videoId, List<?> spoofedFormats) {
|
||||
if (SPOOF_STREAMING_DATA) {
|
||||
try {
|
||||
StreamingDataOuterClass$StreamingData androidStreamingData = streamingDataMap.get(videoId);
|
||||
if (androidStreamingData != null) {
|
||||
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);
|
||||
if (videoId != null && !videoId.equals(MASKED_VIDEO_ID) && spoofedFormats.size() == 0) {
|
||||
List<?> androidFormats = formatsMap.get(videoId);
|
||||
if (androidFormats != null) {
|
||||
Logger.printDebug(() -> "Overriding iOS formats to original formats: " + videoId);
|
||||
return androidFormats;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "getOriginalStreamingData failure", ex);
|
||||
Logger.printException(() -> "getOriginalFormats failure", ex);
|
||||
}
|
||||
}
|
||||
return spoofedStreamingData;
|
||||
return spoofedFormats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Called after {@link #getStreamingData(String, StreamingDataOuterClass$StreamingData)}.
|
||||
* Called after {@link #getStreamingData(String)}.
|
||||
*/
|
||||
@Nullable
|
||||
public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) {
|
||||
|
@ -97,7 +97,7 @@ public class StreamingDataRequest {
|
||||
}
|
||||
|
||||
private final String videoId;
|
||||
private final Future<Pair<ByteBuffer, ClientType>> future;
|
||||
private final Future<ByteBuffer> future;
|
||||
|
||||
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
|
||||
Objects.requireNonNull(playerHeaders);
|
||||
@ -174,7 +174,7 @@ public class StreamingDataRequest {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Pair<ByteBuffer, ClientType> fetch(String videoId, Map<String, String> playerHeaders) {
|
||||
private static ByteBuffer fetch(String videoId, Map<String, String> playerHeaders) {
|
||||
lastSpoofedClientType = null;
|
||||
|
||||
// Retry with different client if empty response body is received.
|
||||
@ -196,7 +196,7 @@ public class StreamingDataRequest {
|
||||
}
|
||||
lastSpoofedClientType = clientType;
|
||||
|
||||
return new Pair<>(ByteBuffer.wrap(baos.toByteArray()), clientType);
|
||||
return ByteBuffer.wrap(baos.toByteArray());
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
@ -214,7 +214,7 @@ public class StreamingDataRequest {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Pair<ByteBuffer, ClientType> getStream() {
|
||||
public ByteBuffer getStream() {
|
||||
try {
|
||||
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException ex) {
|
||||
|
@ -5,7 +5,6 @@ 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.instructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patcher.patch.BytecodePatchBuilder
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
@ -19,6 +18,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.indexOfFirstInstructionOrThrow
|
||||
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
@ -143,8 +143,7 @@ fun baseSpoofStreamingDataPatch(
|
||||
if-eqz v2, :disabled
|
||||
|
||||
# Get streaming data.
|
||||
iget-object v6, p0, $setStreamingDataField
|
||||
invoke-static { v2, v6 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;$STREAMING_DATA_INTERFACE)Ljava/nio/ByteBuffer;
|
||||
invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
|
||||
move-result-object v3
|
||||
if-eqz v3, :disabled
|
||||
|
||||
@ -154,10 +153,17 @@ fun baseSpoofStreamingDataPatch(
|
||||
move-result-object v5
|
||||
check-cast v5, $playerProtoClass
|
||||
|
||||
# Set streaming data.
|
||||
iget-object v6, v5, $getStreamingDataField
|
||||
if-eqz v6, :disabled
|
||||
|
||||
# Get original streaming data.
|
||||
iget-object v0, p0, $setStreamingDataField
|
||||
|
||||
# Set spoofed streaming data.
|
||||
iput-object v6, p0, $setStreamingDataField
|
||||
|
||||
# Set original streaming data formats.
|
||||
invoke-static { v2, v0, v6 }, $EXTENSION_CLASS_DESCRIPTOR->setFormats(Ljava/lang/String;$STREAMING_DATA_INTERFACE$STREAMING_DATA_INTERFACE)V
|
||||
|
||||
:disabled
|
||||
return-void
|
||||
@ -171,41 +177,36 @@ fun baseSpoofStreamingDataPatch(
|
||||
videoStreamingDataConstructorFingerprint.methodOrThrow(videoStreamingDataToStringFingerprint)
|
||||
.apply {
|
||||
val formatStreamModelInitIndex = indexOfFormatStreamModelInitInstruction(this)
|
||||
val getVideoIdIndex =
|
||||
val videoIdIndex =
|
||||
indexOfFirstInstructionReversedOrThrow(formatStreamModelInitIndex) {
|
||||
val reference = getReference<FieldReference>()
|
||||
opcode == Opcode.IGET_OBJECT &&
|
||||
reference?.type == "Ljava/lang/String;" &&
|
||||
reference.definingClass == definingClass
|
||||
}
|
||||
val getVideoIdReference =
|
||||
getInstruction<ReferenceInstruction>(getVideoIdIndex).reference
|
||||
val insertIndex = indexOfFirstInstructionReversedOrThrow(getVideoIdIndex) {
|
||||
val definingClassRegister =
|
||||
getInstruction<TwoRegisterInstruction>(videoIdIndex).registerB
|
||||
val videoIdReference =
|
||||
getInstruction<ReferenceInstruction>(videoIdIndex).reference
|
||||
val formatsIndex = indexOfFirstInstructionReversedOrThrow(videoIdIndex) {
|
||||
opcode == Opcode.IGET_OBJECT &&
|
||||
getReference<FieldReference>()?.definingClass == STREAMING_DATA_INTERFACE
|
||||
}
|
||||
val freeRegister = getInstruction<OneRegisterInstruction>(
|
||||
indexOfFirstInstructionOrThrow(formatsIndex, Opcode.CONST_WIDE)
|
||||
).registerA
|
||||
|
||||
val (freeRegister, streamingDataRegister) = with(
|
||||
getInstruction<TwoRegisterInstruction>(
|
||||
insertIndex
|
||||
)
|
||||
) {
|
||||
Pair(registerA, registerB)
|
||||
}
|
||||
val definingClassRegister =
|
||||
getInstruction<TwoRegisterInstruction>(getVideoIdIndex).registerB
|
||||
val insertReference = getInstruction<ReferenceInstruction>(insertIndex).reference
|
||||
val audioCodecListRegister = getInstruction<TwoRegisterInstruction>(formatsIndex).registerA
|
||||
|
||||
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
|
||||
"""
|
||||
formatsIndex + 1, """
|
||||
# Get video id.
|
||||
iget-object v$freeRegister, v$definingClassRegister, $videoIdReference
|
||||
|
||||
# Override streaming data formats.
|
||||
invoke-static { v$freeRegister, v$audioCodecListRegister }, $EXTENSION_CLASS_DESCRIPTOR->getOriginalFormats(Ljava/lang/String;Ljava/util/List;)Ljava/util/List;
|
||||
move-result-object v$audioCodecListRegister
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user