mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-06-13 05:37:40 +02:00
fix(YouTube - Spoof streaming data): Videos end 1 second early on iOS client
This commit is contained in:
@ -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<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.
|
||||
*/
|
||||
@ -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)}.
|
||||
* <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) {
|
||||
|
@ -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<ByteBuffer> future;
|
||||
private final Future<Pair<ByteBuffer, ClientType>> future;
|
||||
|
||||
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
|
||||
Objects.requireNonNull(playerHeaders);
|
||||
@ -170,7 +172,7 @@ public class StreamingDataRequest {
|
||||
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.
|
||||
@ -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<ByteBuffer, ClientType> getStream() {
|
||||
try {
|
||||
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
|
||||
} 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 {
|
||||
}
|
Reference in New Issue
Block a user