add relocated and shaded protobuf to handle protobuf

This commit is contained in:
oSumAtrIX 2026-03-18 14:26:39 +01:00
parent 025d6daa35
commit 5d80ec9a53
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
18 changed files with 238 additions and 79 deletions

View file

@ -18,8 +18,8 @@ public class SpoofVideoStreamsPatch {
*/
public static void setClientOrderToUse() {
List<ClientType> availableClients = List.of(
ANDROID_VR_1_43_32,
ANDROID_REEL,
ANDROID_VR_1_43_32,
VISIONOS,
ANDROID_VR_1_61_48
);

View file

@ -35,7 +35,7 @@ public class Settings extends YouTubeAndMusicSettings {
// Miscellaneous
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type",
ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
ClientType.ANDROID_REEL, true, parent(SPOOF_VIDEO_STREAMS));
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", TRUE, true);
}

View file

@ -1,5 +1,5 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.android.library)
}
android {
@ -19,4 +19,6 @@ android {
dependencies {
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
compileOnly(libs.protobuf.javalite)
implementation(project(":extensions:shared:protobuf", configuration = "shadowRuntimeElements"))
}

View file

@ -47,7 +47,7 @@ public class BaseSettings {
//
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_video_streams_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
public static final BooleanSetting SANITIZE_SHARING_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE);
public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE);

View file

@ -28,12 +28,11 @@ public enum ClientType {
Build.VERSION.RELEASE,
String.valueOf(Build.VERSION.SDK_INT),
Build.ID,
"20.26.46",
"20.44.38",
// This client has been used by most open-source YouTube stream extraction tools since 2024, including NewPipe Extractor, SmartTube, and Grayjay.
// This client can log in, but if an access token is used in the request, GVS can more easily identify the request as coming from ReVanced.
// This means that the GVS server can strengthen its validation of the ANDROID_REEL client.
// For this reason, ANDROID_REEL is used as a logout client.
false,
true,
true,
false,
"Android Reel"

View file

@ -5,7 +5,6 @@ import android.text.TextUtils;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -39,7 +38,7 @@ public class SpoofVideoStreamsPatch {
@Nullable
private static volatile AppLanguage languageOverride;
private static volatile ClientType preferredClient = ClientType.ANDROID_VR_1_43_32;
private static volatile ClientType preferredClient = ClientType.ANDROID_REEL;
/**
* @return If this patch was included during patching.
@ -250,7 +249,7 @@ public class SpoofVideoStreamsPatch {
* Called after {@link #fetchStreams(String, Map)}.
*/
@Nullable
public static ByteBuffer getStreamingData(String videoId) {
public static byte[] getStreamingData(String videoId) {
if (SPOOF_STREAMING_DATA) {
try {
StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);

View file

@ -1,19 +1,17 @@
package app.revanced.extension.shared.spoof.requests;
import static app.revanced.extension.shared.ByteTrieSearch.convertStringsToBytes;
import static app.revanced.extension.shared.Utils.isNotEmpty;
import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_PLAYER_STREAMING_DATA;
import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_REEL_STREAMING_DATA;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
@ -28,6 +26,10 @@ import java.util.concurrent.TimeoutException;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.innertube.PlayerResponseOuterClass;
import app.revanced.extension.shared.innertube.PlayerResponseOuterClass.PlayerResponse;
import app.revanced.extension.shared.innertube.PlayerResponseOuterClass.StreamingData;
import app.revanced.extension.shared.innertube.ReelItemWatchResponseOuterClass.ReelItemWatchResponse;
import app.revanced.extension.shared.requests.Route;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.spoof.ClientType;
@ -43,7 +45,7 @@ import app.revanced.extension.shared.spoof.ClientType;
*/
public class StreamingDataRequest {
private static volatile ClientType[] clientOrderToUse = ClientType.values();
private static volatile ClientType[] clientOrderToUse = ClientType.values();
public static void setClientOrderToUse(List<ClientType> availableClients, ClientType preferredClient) {
Objects.requireNonNull(preferredClient);
@ -113,7 +115,7 @@ public class StreamingDataRequest {
private final String videoId;
private final Future<ByteBuffer> future;
private final Future<byte[]> future;
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
Objects.requireNonNull(playerHeaders);
@ -136,6 +138,12 @@ public class StreamingDataRequest {
Logger.printInfo(() -> toastMessage, ex);
}
private static void handleDebugToast(String toastMessage, ClientType clientType) {
if (BaseSettings.DEBUG.get() && BaseSettings.DEBUG_TOAST_ON_ERROR.get()) {
Utils.showToastShort(String.format(toastMessage, clientType));
}
}
@Nullable
private static HttpURLConnection send(ClientType clientType,
String videoId,
@ -208,7 +216,7 @@ public class StreamingDataRequest {
return null;
}
private static ByteBuffer fetch(String videoId, Map<String, String> playerHeaders) {
private static byte[] fetch(String videoId, Map<String, String> playerHeaders) {
final boolean debugEnabled = BaseSettings.DEBUG.get();
// Retry with different client if empty response body is received.
@ -219,33 +227,11 @@ public class StreamingDataRequest {
HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
if (connection != null) {
try {
// gzip encoding doesn't response with content length (-1),
// but empty response body does.
if (connection.getContentLength() == 0) {
if (BaseSettings.DEBUG.get() && BaseSettings.DEBUG_TOAST_ON_ERROR.get()) {
Utils.showToastShort("Debug: Ignoring empty spoof stream client " + clientType);
}
} else {
try (InputStream inputStream = new BufferedInputStream(connection.getInputStream());
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] playerResponseBuffer = buildPlayerResponseBuffer(clientType, connection);
if (playerResponseBuffer != null) {
lastSpoofedClientType = clientType;
byte[] buffer = new byte[2048];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) >= 0) {
baos.write(buffer, 0, bytesRead);
}
if (clientType == ClientType.ANDROID_CREATOR && liveStreamBufferSearch.matches(buffer)) {
Logger.printDebug(() -> "Skipping Android Studio as video is a livestream: " + videoId);
} else {
lastSpoofedClientType = clientType;
return ByteBuffer.wrap(baos.toByteArray());
}
}
}
} catch (IOException ex) {
Logger.printException(() -> "Fetch failed while processing response data", ex);
return playerResponseBuffer;
}
}
}
@ -255,12 +241,61 @@ public class StreamingDataRequest {
return null;
}
@Nullable
private static byte[] buildPlayerResponseBuffer(ClientType clientType,
HttpURLConnection connection) {
// gzip encoding doesn't response with content length (-1),
// but empty response body does.
if (connection.getContentLength() == 0) {
handleDebugToast("Debug: Ignoring empty spoof stream client (%s)", clientType);
return null;
}
try (InputStream inputStream = connection.getInputStream()) {
PlayerResponse playerResponse = clientType.usePlayerEndpoint
? PlayerResponse.parseFrom(inputStream)
: ReelItemWatchResponse.parseFrom(inputStream).getPlayerResponse();
var playabilityStatus = playerResponse.getPlayabilityStatus();
if (playabilityStatus.getStatus() != PlayerResponseOuterClass.Status.OK) {
handleDebugToast("Debug: Ignoring unplayable video (%s)", clientType);
String reason = playabilityStatus.getReason();
if (isNotEmpty(reason)) {
Logger.printDebug(() -> String.format("Debug: Ignoring unplayable video (%s), reason: %s", clientType, reason));
}
return null;
}
PlayerResponse.Builder responseBuilder = playerResponse.toBuilder();
if (!playerResponse.hasStreamingData()) {
handleDebugToast("Debug: Ignoring empty streaming data (%s)", clientType);
return null;
}
// Android Studio only supports the HLS protocol for live streams.
// HLS protocol can theoretically be played with ExoPlayer,
// but the related code has not yet been implemented.
// If DASH protocol is not available, the client will be skipped.
StreamingData streamingData = playerResponse.getStreamingData();
if (streamingData.getAdaptiveFormatsCount() == 0) {
handleDebugToast("Debug: Ignoring empty adaptiveFormat (%s)", clientType);
return null;
}
return responseBuilder.build().toByteArray();
} catch (IOException ex) {
Logger.printException(() -> "Failed to write player response to buffer array", ex);
return null;
}
}
public boolean fetchCompleted() {
return future.isDone();
}
@Nullable
public ByteBuffer getStream() {
public byte[] getStream() {
try {
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) {

View file

@ -0,0 +1,55 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
kotlin("jvm")
alias(libs.plugins.protobuf)
alias(libs.plugins.shadow)
}
val shade: Configuration by configurations.creating {
configurations.getByName("compileClasspath").extendsFrom(this)
configurations.getByName("runtimeClasspath").extendsFrom(this)
}
dependencies {
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
shade(libs.protobuf.javalite)
}
sourceSets {
// Make sure generated proto sources are compiled and end up in the shaded jar
main {
java.srcDir("$buildDir/generated/source/proto/main/java")
}
}
protobuf {
protoc {
artifact = libs.protobuf.protoc.get().toString()
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
named("java") {
option("lite")
}
}
}
}
}
val shadowJar = tasks.named<ShadowJar>("shadowJar") {
configurations = listOf(shade)
relocate("com.google.protobuf", "app.revanced.com.google.protobuf")
}
configurations.named("runtimeElements") {
isCanBeConsumed = true
isCanBeResolved = false
outgoing.artifacts.clear()
outgoing.artifact(shadowJar)
}!!.let { artifacts { add(it.name, shadowJar) } }

View file

@ -0,0 +1,42 @@
syntax = "proto3";
package app.revanced.extension.shared.innertube;
option optimize_for = LITE_RUNTIME;
option java_package = "app.revanced.extension.shared.innertube";
message PlayerResponse {
oneof data {
PlayabilityStatus playability_status = 2;
StreamingData streaming_data = 4;
}
}
message PlayabilityStatus {
Status status = 1;
string reason = 2;
}
enum Status {
OK = 0;
ERROR = 1;
UNPLAYABLE = 2;
LOGIN_REQUIRED = 3;
CONTENT_CHECK_REQUIRED = 4;
AGE_CHECK_REQUIRED = 5;
LIVE_STREAM_OFFLINE = 6;
FULLSCREEN_ONLY = 7;
GL_PLAYBACK_REQUIRED = 8;
AGE_VERIFICATION_REQUIRED = 9;
}
message StreamingData {
repeated Format formats = 2;
repeated Format adaptiveFormats = 3;
string serverAbrStreamingUrl = 15;
}
message Format {
string url = 2;
string signatureCipher = 48;
}

View file

@ -0,0 +1,14 @@
syntax = "proto3";
import "app/revanced/extension/shared/innertube/player_response.proto";
package app.revanced.extension.shared.innertube;
option optimize_for = LITE_RUNTIME;
option java_package = "app.revanced.extension.shared.innertube";
message ReelItemWatchResponse {
oneof data {
PlayerResponse player_response = 4;
}
}

View file

@ -43,8 +43,8 @@ public class SpoofVideoStreamsPatch {
}
List<ClientType> availableClients = List.of(
ANDROID_VR_1_43_32,
ANDROID_REEL,
ANDROID_VR_1_43_32,
VISIONOS,
ANDROID_CREATOR
);

View file

@ -19,7 +19,7 @@ import app.revanced.extension.shared.spoof.ClientType;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"deprecation", "unused"})
public class SpoofStreamingDataSideEffectsPreference extends Preference {
public class SpoofVideoStreamsSideEffectsPreference extends Preference {
@Nullable
private ClientType currentClientType;
@ -33,19 +33,19 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
Utils.runOnMainThread(this::updateUI);
};
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
public SpoofVideoStreamsSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
public SpoofVideoStreamsSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs) {
public SpoofVideoStreamsSideEffectsPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SpoofStreamingDataSideEffectsPreference(Context context) {
public SpoofVideoStreamsSideEffectsPreference(Context context) {
super(context);
}
@ -92,12 +92,13 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
summary = str("revanced_spoof_video_streams_about_playback_failure");
// VR 1.61 is not exposed in the UI and should never be reached here.
case ANDROID_VR_1_43_32, ANDROID_VR_1_61_48 ->
summary = str("revanced_spoof_video_streams_about_no_audio_tracks")
+ '\n' + str("revanced_spoof_video_streams_about_no_stable_volume");
case VISIONOS ->
summary = str("revanced_spoof_video_streams_about_experimental")
summary = str("revanced_spoof_video_streams_about_playback_failure")
+ '\n' + str("revanced_spoof_video_streams_about_no_audio_tracks")
+ '\n' + str("revanced_spoof_video_streams_about_no_av1");
+ '\n' + str("revanced_spoof_video_streams_about_no_stable_volume");
case VISIONOS -> summary = str("revanced_spoof_video_streams_about_experimental")
+ '\n' + str("revanced_spoof_video_streams_about_playback_failure")
+ '\n' + str("revanced_spoof_video_streams_about_no_audio_tracks")
+ '\n' + str("revanced_spoof_video_streams_about_no_av1");
default -> Logger.printException(() -> "Unknown client: " + clientType);
}

View file

@ -10,6 +10,10 @@ okhttp = "5.3.2"
retrofit = "3.0.0"
guava = "33.5.0-jre"
apksig = "9.0.1"
# TODO: Adjust once https://github.com/google/protobuf-gradle-plugin/pull/797 is merged.
protobuf = "master-SNAPSHOT"
protoc = "4.34.0"
shadow = "9.4.0"
[libraries]
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
@ -18,6 +22,10 @@ okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
apksig = { group = "com.android.tools.build", name = "apksig", version.ref = "apksig" }
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protoc" }
protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protoc" }
[plugins]
android-library = { id = "com.android.library" }
protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }

View file

@ -169,13 +169,13 @@ internal fun spoofVideoStreamsPatch(
if-eqz v2, :disabled
# Get streaming data.
invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)[B
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
invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}[B)$protobufClass
move-result-object v5
check-cast v5, $playerProtoClass

View file

@ -63,10 +63,10 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch(
// Requires a key and title but the actual text is chosen at runtime.
key = "revanced_spoof_video_streams_about",
summaryKey = null,
tag = "app.revanced.extension.youtube.settings.preference.SpoofStreamingDataSideEffectsPreference",
tag = "app.revanced.extension.youtube.settings.preference.SpoofVideoStreamsSideEffectsPreference",
),
SwitchPreference("revanced_spoof_video_streams_av1"),
SwitchPreference("revanced_spoof_streaming_data_stats_for_nerds"),
SwitchPreference("revanced_spoof_video_streams_stats_for_nerds"),
),
),
)

View file

@ -265,14 +265,14 @@
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
<string-array name="revanced_spoof_video_streams_client_type_entries">
<item>Android Reel</item>
<item>Android Studio</item>
<item>Android VR</item>
<item>Android Studio</item>
<item>visionOS</item>
</string-array>
<string-array name="revanced_spoof_video_streams_client_type_entry_values">
<item>ANDROID_REEL</item>
<item>ANDROID_CREATOR</item>
<item>ANDROID_VR_1_43_32</item>
<item>ANDROID_CREATOR</item>
<item>VISIONOS</item>
</string-array>
</patch>

View file

@ -144,6 +144,16 @@ If you are a YouTube Premium user, this setting may not be required"</string>
Playback may not work"</string>
<string name="revanced_spoof_video_streams_user_dialog_message">Turning off this setting may cause playback issues.</string>
<string name="revanced_spoof_video_streams_client_type_title">Default client</string>
<string name="revanced_spoof_video_streams_about_experimental">• Experimental client and may stop working anytime</string>
<string name="revanced_spoof_video_streams_about_playback_failure">• Video may stop at 1:00, or may not be available in some regions</string>
<string name="revanced_spoof_video_streams_about_no_audio_tracks">• Audio track menu is missing</string>
<string name="revanced_spoof_video_streams_about_no_av1">• No AV1 video codec</string>
<string name="revanced_spoof_video_streams_about_no_immersive_mode">• 360° VR immersive mode is not available</string>
<string name="revanced_spoof_video_streams_about_no_stable_volume">• Stable volume is not available</string>
<string name="revanced_spoof_video_streams_about_title">Spoofing side effects</string>
<!-- "Force original audio" should use the same text as revanced_force_original_audio_title -->
<string name="revanced_spoof_video_streams_about_no_force_original_audio">• Force original audio is not available</string>
</patch>
<patch id="misc.audio.forceOriginalAudioPatch">
<string name="revanced_force_original_audio_title">Force original audio language</string>
@ -1786,18 +1796,9 @@ Playback may stutter or drop frames"</string>
<string name="revanced_spoof_video_streams_av1_user_dialog_message">"Enabling this setting may use software AV1 decoding.
Video playback with AV1 may stutter or drop frames."</string>
<string name="revanced_spoof_video_streams_about_title">Spoofing side effects</string>
<string name="revanced_spoof_video_streams_about_experimental">• Experimental client and may stop working anytime</string>
<string name="revanced_spoof_video_streams_about_playback_failure">• Video may stop at 1:00, or may not be available in some regions</string>
<string name="revanced_spoof_video_streams_about_no_audio_tracks">• Audio track menu is missing</string>
<string name="revanced_spoof_video_streams_about_no_av1">• No AV1 video codec</string>
<string name="revanced_spoof_video_streams_about_no_stable_volume">• Stable volume is not available</string>
<string name="revanced_spoof_video_streams_about_kids_videos">• Kids videos may not play when logged out or in incognito mode</string>
<!-- "Force original audio" should use the same text as revanced_force_original_audio_title -->
<string name="revanced_spoof_video_streams_about_no_force_original_audio">• Force original audio is not available</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_title">Show in Stats for nerds</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_on">Client type is shown in Stats for nerds</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Client is hidden in Stats for nerds</string>
<string name="revanced_spoof_video_streams_stats_for_nerds_title">Show in Stats for nerds</string>
<string name="revanced_spoof_video_streams_stats_for_nerds_summary_on">Client type is shown in Stats for nerds</string>
<string name="revanced_spoof_video_streams_stats_for_nerds_summary_off">Client is hidden in Stats for nerds</string>
</patch>
</app>
<app id="music">

View file

@ -2,7 +2,6 @@ rootProject.name = "revanced-patches"
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
google()
maven {
@ -10,12 +9,16 @@ pluginManagement {
url = uri("https://maven.pkg.github.com/revanced/revanced-patches-gradle-plugin")
credentials(PasswordCredentials::class)
}
// TODO: Remove once https://github.com/google/protobuf-gradle-plugin/pull/797 is merged.
maven { url = uri("https://jitpack.io") }
}
}
dependencyResolutionManagement {
repositories {
mavenLocal()
// TODO: Remove once https://github.com/google/protobuf-gradle-plugin/pull/797 is merged.
resolutionStrategy {
eachPlugin {
if (requested.id.id == "com.google.protobuf") {
useModule("com.github.ReVanced:protobuf-gradle-plugin:${requested.version}")
}
}
}
}
@ -33,4 +36,4 @@ settings {
}
}
include(":patches:stub")
include(":patches:stub")