mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-05-29 05:10:20 +02:00
fix(YouTube - Spoof streaming data): Videos end 1 second early on iOS client
This commit is contained in:
parent
bb1946d9db
commit
e0b6d33df5
@ -5,10 +5,16 @@ import android.text.TextUtils;
|
|||||||
|
|
||||||
import androidx.annotation.Nullable;
|
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.nio.ByteBuffer;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import app.revanced.extension.shared.patches.BlockRequestPatch;
|
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.Logger;
|
||||||
import app.revanced.extension.shared.utils.Utils;
|
import app.revanced.extension.shared.utils.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
@ -17,6 +23,34 @@ import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest
|
|||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key: videoId
|
||||||
|
* value: android StreamingData
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key: android StreamingData
|
||||||
|
* value: fetched ClientType
|
||||||
|
*/
|
||||||
|
private static final Map<StreamingDataOuterClass$StreamingData, ClientType> 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.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
@ -66,9 +100,11 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
|||||||
* Injection point.
|
* Injection point.
|
||||||
* Fix playback by replace the streaming data.
|
* Fix playback by replace the streaming data.
|
||||||
* Called after {@link #fetchStreams(String, Map)}.
|
* Called after {@link #fetchStreams(String, Map)}.
|
||||||
|
*
|
||||||
|
* @param originalStreamingData Original StreamingData.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public static ByteBuffer getStreamingData(String videoId) {
|
public static ByteBuffer getStreamingData(String videoId, StreamingDataOuterClass$StreamingData originalStreamingData) {
|
||||||
if (SPOOF_STREAMING_DATA) {
|
if (SPOOF_STREAMING_DATA) {
|
||||||
try {
|
try {
|
||||||
StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);
|
StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);
|
||||||
@ -85,7 +121,11 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
|||||||
var stream = request.getStream();
|
var stream = request.getStream();
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
Logger.printDebug(() -> "Overriding video stream: " + videoId);
|
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.
|
* Injection point.
|
||||||
* Called after {@link #getStreamingData(String)}.
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* To fix this, replace streamingData (spoofedStreamingData) with originalStreamingData, which is only used to initialize the {@link FormatStreamModel} class to measure the video length.
|
||||||
|
* <p>
|
||||||
|
* 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
|
@Nullable
|
||||||
public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) {
|
public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) {
|
||||||
|
@ -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 static app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA;
|
||||||
|
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
@ -93,7 +95,7 @@ public class StreamingDataRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final String videoId;
|
private final String videoId;
|
||||||
private final Future<ByteBuffer> future;
|
private final Future<Pair<ByteBuffer, ClientType>> future;
|
||||||
|
|
||||||
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
|
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
|
||||||
Objects.requireNonNull(playerHeaders);
|
Objects.requireNonNull(playerHeaders);
|
||||||
@ -170,7 +172,7 @@ public class StreamingDataRequest {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ByteBuffer fetch(String videoId, Map<String, String> playerHeaders) {
|
private static Pair<ByteBuffer, ClientType> fetch(String videoId, Map<String, String> playerHeaders) {
|
||||||
lastSpoofedClientType = null;
|
lastSpoofedClientType = null;
|
||||||
|
|
||||||
// Retry with different client if empty response body is received.
|
// Retry with different client if empty response body is received.
|
||||||
@ -193,7 +195,7 @@ public class StreamingDataRequest {
|
|||||||
}
|
}
|
||||||
lastSpoofedClientType = clientType;
|
lastSpoofedClientType = clientType;
|
||||||
|
|
||||||
return ByteBuffer.wrap(baos.toByteArray());
|
return new Pair<>(ByteBuffer.wrap(baos.toByteArray()), clientType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
@ -211,7 +213,7 @@ public class StreamingDataRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public ByteBuffer getStream() {
|
public Pair<ByteBuffer, ClientType> getStream() {
|
||||||
try {
|
try {
|
||||||
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
|
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
|
||||||
} catch (TimeoutException ex) {
|
} catch (TimeoutException ex) {
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
package com.google.android.libraries.youtube.innertube.model.media;
|
||||||
|
|
||||||
|
public class FormatStreamModel {
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package com.google.protos.youtube.api.innertube;
|
||||||
|
|
||||||
|
public class StreamingDataOuterClass$StreamingData {
|
||||||
|
}
|
@ -34,6 +34,10 @@ 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 = {},
|
||||||
@ -130,7 +134,8 @@ fun baseSpoofStreamingDataPatch(
|
|||||||
if-eqz v2, :disabled
|
if-eqz v2, :disabled
|
||||||
|
|
||||||
# Get streaming data.
|
# 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
|
move-result-object v3
|
||||||
if-eqz v3, :disabled
|
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<FieldReference>()
|
||||||
|
opcode == Opcode.IGET_OBJECT &&
|
||||||
|
reference?.type == "Ljava/lang/String;" &&
|
||||||
|
reference.definingClass == definingClass
|
||||||
|
}
|
||||||
|
val getVideoIdReference = getInstruction<ReferenceInstruction>(getVideoIdIndex).reference
|
||||||
|
val insertIndex = indexOfFirstInstructionReversedOrThrow(getVideoIdIndex) {
|
||||||
|
opcode == Opcode.IGET_OBJECT &&
|
||||||
|
getReference<FieldReference>()?.definingClass == STREAMING_DATA_INTERFACE
|
||||||
|
}
|
||||||
|
|
||||||
|
val (freeRegister, streamingDataRegister) = with(getInstruction<TwoRegisterInstruction>(insertIndex)) {
|
||||||
|
Pair(registerA, registerB)
|
||||||
|
}
|
||||||
|
val definingClassRegister = getInstruction<TwoRegisterInstruction>(getVideoIdIndex).registerB
|
||||||
|
val insertReference = getInstruction<ReferenceInstruction>(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
|
// endregion
|
||||||
|
|
||||||
// region Remove /videoplayback request body to fix playback.
|
// region Remove /videoplayback request body to fix playback.
|
||||||
|
@ -103,6 +103,34 @@ internal val protobufClassParseByteBufferFingerprint = legacyFingerprint(
|
|||||||
customFingerprint = { method, _ -> method.name == "parseFrom" },
|
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<MethodReference>()
|
||||||
|
opcode == Opcode.INVOKE_DIRECT &&
|
||||||
|
reference?.name == "<init>" &&
|
||||||
|
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 const val HLS_CURRENT_TIME_FEATURE_FLAG = 45355374L
|
||||||
|
|
||||||
internal val hlsCurrentTimeFingerprint = legacyFingerprint(
|
internal val hlsCurrentTimeFingerprint = legacyFingerprint(
|
||||||
|
@ -1905,7 +1905,7 @@ Tap on the continue button and disable battery optimizations."</string>
|
|||||||
<string name="revanced_spoof_streaming_data_type_entry_android_unplugged">Android TV</string>
|
<string name="revanced_spoof_streaming_data_type_entry_android_unplugged">Android TV</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">• Videos may end 1 second early.</string>
|
<string name="revanced_spoof_streaming_data_side_effects_ios">• Not yet found.</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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user