Skip to content

Commit

Permalink
fix(YouTube - Spoof streaming data): Videos end 1 second early on iOS…
Browse files Browse the repository at this point in the history
… client
  • Loading branch information
inotia00 authored and anddea committed Dec 17, 2024
1 parent 6323421 commit b2cc033
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@

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;
Expand All @@ -17,6 +23,34 @@
@SuppressWarnings("unused")
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.
*/
Expand Down Expand Up @@ -66,9 +100,11 @@ public static void fetchStreams(String url, Map<String, String> requestHeaders)
* 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);
Expand All @@ -85,7 +121,11 @@ public static ByteBuffer getStreamingData(String videoId) {
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;
}
}

Expand All @@ -100,7 +140,42 @@ public static ByteBuffer getStreamingData(String videoId) {

/**
* 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
public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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;

Expand Down Expand Up @@ -93,7 +95,7 @@ public static String getLastSpoofedClientName() {
}

private final String videoId;
private final Future<ByteBuffer> future;
private final Future<Pair<ByteBuffer, ClientType>> future;

private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
Objects.requireNonNull(playerHeaders);
Expand Down Expand Up @@ -170,7 +172,7 @@ private static HttpURLConnection send(ClientType clientType, String videoId,
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;

// Retry with different client if empty response body is received.
Expand All @@ -193,7 +195,7 @@ private static ByteBuffer fetch(String videoId, Map<String, String> playerHeader
}
lastSpoofedClientType = clientType;

return ByteBuffer.wrap(baos.toByteArray());
return new Pair<>(ByteBuffer.wrap(baos.toByteArray()), clientType);
}
}
} catch (IOException ex) {
Expand All @@ -211,7 +213,7 @@ public boolean fetchCompleted() {
}

@Nullable
public ByteBuffer getStream() {
public Pair<ByteBuffer, ClientType> getStream() {
try {
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.google.android.libraries.youtube.innertube.model.media;

public class FormatStreamModel {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.google.protos.youtube.api.innertube;

public class StreamingDataOuterClass$StreamingData {
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package app.revanced.patches.shared.spoof.streamingdata

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.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
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.shared.blockrequest.blockRequestPatch
import app.revanced.patches.shared.extension.Constants.SPOOF_PATH
import app.revanced.util.findInstructionIndicesReversedOrThrow
Expand All @@ -17,15 +19,25 @@ 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 com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
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 = {},
Expand Down Expand Up @@ -71,6 +83,8 @@ fun baseSpoofStreamingDataPatch(

createStreamingDataFingerprint.matchOrThrow(createStreamingDataParentFingerprint).let { result ->
result.method.apply {
val setStreamDataMethodName = "patch_setStreamingData"
val resultMethodType = result.classDef.type
val setStreamingDataIndex = result.patternMatch!!.startIndex
val setStreamingDataField =
getInstruction(setStreamingDataIndex).getReference<FieldReference>().toString()
Expand All @@ -87,50 +101,97 @@ fun baseSpoofStreamingDataPatch(
?: throw PatchException("Could not find getStreamingDataField")

val videoDetailsIndex = result.patternMatch!!.endIndex
val videoDetailsRegister = getInstruction<TwoRegisterInstruction>(videoDetailsIndex).registerA
val videoDetailsClass =
getInstruction(videoDetailsIndex).getReference<FieldReference>()!!.type

val insertIndex = videoDetailsIndex + 1
val videoDetailsRegister =
getInstruction<TwoRegisterInstruction>(videoDetailsIndex).registerA

val overrideRegister = getInstruction<TwoRegisterInstruction>(insertIndex).registerA
val freeRegister = implementation!!.registerCount - parameters.size - 2
addInstruction(
videoDetailsIndex + 1,
"invoke-direct { p0, v$videoDetailsRegister }, " +
"$resultMethodType->$setStreamDataMethodName($videoDetailsClass)V",
)

addInstructionsWithLabels(
insertIndex,
"""
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z
move-result v$freeRegister
if-eqz v$freeRegister, :disabled
# Get video id.
# From YouTube 17.34.36 to YouTube 19.16.39, the field names and field types are the same.
iget-object v$freeRegister, v$videoDetailsRegister, $videoDetailsClass->c:Ljava/lang/String;
if-eqz v$freeRegister, :disabled
# Get streaming data.
invoke-static { v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
move-result-object v$freeRegister
if-eqz v$freeRegister, :disabled
# Parse streaming data.
sget-object v$overrideRegister, $playerProtoClass->a:$playerProtoClass
invoke-static { v$overrideRegister, v$freeRegister }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass
move-result-object v$freeRegister
check-cast v$freeRegister, $playerProtoClass
# Set streaming data.
iget-object v$freeRegister, v$freeRegister, $getStreamingDataField
if-eqz v$freeRegister, :disabled
iput-object v$freeRegister, p0, $setStreamingDataField
""",
ExternalLabel("disabled", getInstruction(insertIndex))
result.classDef.methods.add(
ImmutableMethod(
resultMethodType,
setStreamDataMethodName,
listOf(ImmutableMethodParameter(videoDetailsClass, annotations, "videoDetails")),
"V",
AccessFlags.PRIVATE.value or AccessFlags.FINAL.value,
annotations,
null,
MutableMethodImplementation(9),
).toMutable().apply {
addInstructionsWithLabels(
0,
"""
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z
move-result v0
if-eqz v0, :disabled
# Get video id.
iget-object v2, p1, $videoDetailsClass->c:Ljava/lang/String;
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;
move-result-object v3
if-eqz v3, :disabled
# Parse streaming data.
sget-object v4, $playerProtoClass->a:$playerProtoClass
invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass
move-result-object v5
check-cast v5, $playerProtoClass
# Set streaming data.
iget-object v6, v5, $getStreamingDataField
if-eqz v6, :disabled
iput-object v6, p0, $setStreamingDataField
:disabled
return-void
""",
)
},
)
}
}

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

// region Remove /videoplayback request body to fix playback.
Expand Down
Loading

0 comments on commit b2cc033

Please sign in to comment.