diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java index ade26a30fb..46c85a8edd 100644 --- a/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java @@ -1,7 +1,7 @@ package app.revanced.extension.music.patches.spoof; import static app.revanced.extension.music.settings.Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE; -import static app.revanced.extension.shared.spoof.ClientType.ANDROID_NO_SDK; +import static app.revanced.extension.shared.spoof.ClientType.ANDROID_REEL; import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32; import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48; import static app.revanced.extension.shared.spoof.ClientType.VISIONOS; @@ -18,8 +18,8 @@ public class SpoofVideoStreamsPatch { */ public static void setClientOrderToUse() { List availableClients = List.of( + ANDROID_REEL, ANDROID_VR_1_43_32, - ANDROID_NO_SDK, VISIONOS, ANDROID_VR_1_61_48 ); diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java index b2f61541a4..7decd29b8a 100644 --- a/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java +++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java @@ -35,7 +35,7 @@ public class Settings extends YouTubeAndMusicSettings { // Miscellaneous public static final EnumSetting 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); } diff --git a/extensions/shared/library/build.gradle.kts b/extensions/shared/library/build.gradle.kts index 100de7ae14..8215e513ad 100644 --- a/extensions/shared/library/build.gradle.kts +++ b/extensions/shared/library/build.gradle.kts @@ -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")) } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java index 2a3aef407f..5fc4418366 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java @@ -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); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java index 39076b562d..9abd430719 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java @@ -9,9 +9,34 @@ import java.util.Locale; import java.util.Objects; import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; @SuppressWarnings("ConstantLocale") public enum ClientType { + /** + * Video not playable: Paid, Movie, Private, Age-restricted. + * Uses non-adaptive bitrate. + * AV1 codec available. + */ + ANDROID_REEL( + 3, + "ANDROID", + "com.google.android.youtube", + Build.MANUFACTURER, + Build.MODEL, + "Android", + Build.VERSION.RELEASE, + String.valueOf(Build.VERSION.SDK_INT), + Build.ID, + "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. + true, + true, + false, + "Android Reel" + ), /** * Video not playable: Kids / Paid / Movie / Private / Age-restricted. * This client can only be used when logged out. @@ -28,10 +53,10 @@ public enum ClientType { // Android 12.1 "32", "SQ3A.220605.009.A1", - "132.0.6808.3", "1.61.48", false, false, + true, "Android VR 1.61" ), /** @@ -48,39 +73,12 @@ public enum ClientType { ANDROID_VR_1_61_48.osVersion, Objects.requireNonNull(ANDROID_VR_1_61_48.androidSdkVersion), Objects.requireNonNull(ANDROID_VR_1_61_48.buildId), - "107.0.5284.2", "1.43.32", ANDROID_VR_1_61_48.useAuth, ANDROID_VR_1_61_48.supportsMultiAudioTracks, + ANDROID_VR_1_61_48.usePlayerEndpoint, "Android VR 1.43" ), - /** - * Video not playable: Paid / Movie / Private / Age-restricted. - * Note: The 'Authorization' key must be excluded from the header. - * - * According to TeamNewPipe in 2022, if the 'androidSdkVersion' field is missing, - * the GVS did not return a valid response: - * [NewPipe#8713 (comment)](https://github.com/TeamNewPipe/NewPipe/issues/8713#issuecomment-1207443550). - * - * According to the latest commit in yt-dlp, the GVS returns a valid response - * even if the 'androidSdkVersion' field is missing: - * [yt-dlp#14693](https://github.com/yt-dlp/yt-dlp/pull/14693). - * - * For some reason, PoToken is not required. - */ - ANDROID_NO_SDK( - 3, - "ANDROID", - "", - "", - "", - Build.VERSION.RELEASE, - "20.05.46", - "com.google.android.youtube/20.05.46 (Linux; U; Android " + Build.VERSION.RELEASE + ") gzip", - false, - true, - "Android No SDK" - ), /** * Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children". * Google Pixel 9 Pro Fold @@ -95,10 +93,10 @@ public enum ClientType { "15", "35", "AP3A.241005.015.A2", - "132.0.6779.0", "23.47.101", true, false, + true, "Android Studio" ), /** @@ -114,32 +112,8 @@ public enum ClientType { "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", false, false, - "visionOS" - ), - /** - * The device machine id for the iPad 6th Gen (iPad7,6). - * AV1 hardware decoding is not supported. - * See [this GitHub Gist](https://gist.github.com/adamawolf/3048717) for more information. - * - * Based on Google's actions to date, PoToken may not be required on devices with very low specs. - * For example, suppose the User-Agent for a PlayStation 3 (with 256MB of RAM) is used. - * Accessing 'Web' (https://www.youtube.com) will redirect to 'TV' (https://www.youtube.com/tv). - * 'TV' target devices with very low specs, such as embedded devices, game consoles, and blu-ray players, so PoToken is not required. - * - * For this reason, the device machine id for the iPad 6th Gen (with 2GB of RAM), - * the lowest spec device capable of running iPadOS 17, was used. - */ - IPADOS(5, - "IOS", - "Apple", - "iPad7,6", - "iPadOS", - "17.7.10.21H450", - "19.22.3", - "com.google.ios.youtube/19.22.3 (iPad7,6; U; CPU iPadOS 17_7_10 like Mac OS X; " + Locale.getDefault() + ")", - false, true, - "iPadOS" + "visionOS" ); /** @@ -195,13 +169,6 @@ public enum ClientType { @Nullable private final String buildId; - /** - * Cronet release version, as found in decompiled client apk. - * Field is null if not applicable. - */ - @Nullable - private final String cronetVersion; - /** * App version. */ @@ -217,6 +184,11 @@ public enum ClientType { */ public final boolean supportsMultiAudioTracks; + /** + * If the client should use the player endpoint for stream extraction. + */ + public final boolean usePlayerEndpoint; + /** * Friendly name displayed in stats for nerds. */ @@ -234,10 +206,10 @@ public enum ClientType { String osVersion, @NonNull String androidSdkVersion, @NonNull String buildId, - @NonNull String cronetVersion, String clientVersion, boolean useAuth, boolean supportsMultiAudioTracks, + boolean usePlayerEndpoint, String friendlyName) { this.id = id; this.clientName = clientName; @@ -248,21 +220,20 @@ public enum ClientType { this.osVersion = osVersion; this.androidSdkVersion = androidSdkVersion; this.buildId = buildId; - this.cronetVersion = cronetVersion; this.clientVersion = clientVersion; this.useAuth = useAuth; this.supportsMultiAudioTracks = supportsMultiAudioTracks; + this.usePlayerEndpoint = usePlayerEndpoint; this.friendlyName = friendlyName; Locale defaultLocale = Locale.getDefault(); - this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)", + this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s)", packageName, clientVersion, osVersion, defaultLocale, deviceModel, - Objects.requireNonNull(buildId), - Objects.requireNonNull(cronetVersion) + buildId ); Logger.printDebug(() -> "userAgent: " + this.userAgent); } @@ -278,6 +249,7 @@ public enum ClientType { String userAgent, boolean useAuth, boolean supportsMultiAudioTracks, + boolean usePlayerEndpoint, String friendlyName) { this.id = id; this.clientName = clientName; @@ -289,10 +261,10 @@ public enum ClientType { this.userAgent = userAgent; this.useAuth = useAuth; this.supportsMultiAudioTracks = supportsMultiAudioTracks; + this.usePlayerEndpoint = usePlayerEndpoint; this.friendlyName = friendlyName; this.packageName = null; this.androidSdkVersion = null; this.buildId = null; - this.cronetVersion = null; } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java index 38fbac9938..0c861510fe 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java @@ -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); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java index 31e3f03034..959048d1e2 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java @@ -15,13 +15,20 @@ import app.revanced.extension.shared.spoof.ClientType; import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch; final class PlayerRoutes { - static final Route.CompiledRoute GET_STREAMING_DATA = new Route( + static final Route.CompiledRoute GET_PLAYER_STREAMING_DATA = new Route( Route.Method.POST, "player" + "?fields=streamingData" + "&alt=proto" ).compile(); + static final Route.CompiledRoute GET_REEL_STREAMING_DATA = new Route( + Route.Method.POST, + "reel/reel_item_watch" + + "?fields=playerResponse.playabilityStatus,playerResponse.streamingData" + + "&alt=proto" + ).compile(); + private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"; /** @@ -47,6 +54,7 @@ final class PlayerRoutes { Locale streamLocale = language.getLocale(); JSONObject client = new JSONObject(); + client.put("deviceMake", clientType.deviceMake); client.put("deviceModel", clientType.deviceModel); client.put("clientName", clientType.clientName); @@ -61,9 +69,19 @@ final class PlayerRoutes { context.put("client", client); innerTubeBody.put("context", context); - innerTubeBody.put("contentCheckOk", true); - innerTubeBody.put("racyCheckOk", true); - innerTubeBody.put("videoId", videoId); + + if (clientType.usePlayerEndpoint) { + innerTubeBody.put("contentCheckOk", true); + innerTubeBody.put("racyCheckOk", true); + innerTubeBody.put("videoId", videoId); + } else { + JSONObject playerRequest = new JSONObject(); + playerRequest.put("contentCheckOk", true); + playerRequest.put("racyCheckOk", true); + playerRequest.put("videoId", videoId); + innerTubeBody.put("playerRequest", playerRequest); + innerTubeBody.put("disablePlayerResponse", false); + } } catch (JSONException e) { Logger.printException(() -> "Failed to create innerTubeBody", e); } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java index 8eb1eaaab5..fb8a8e79e8 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java @@ -1,18 +1,17 @@ package app.revanced.extension.shared.spoof.requests; import static app.revanced.extension.shared.ByteTrieSearch.convertStringsToBytes; -import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_STREAMING_DATA; +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; @@ -27,6 +26,11 @@ 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; @@ -41,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 availableClients, ClientType preferredClient) { Objects.requireNonNull(preferredClient); @@ -111,7 +115,7 @@ public class StreamingDataRequest { private final String videoId; - private final Future future; + private final Future future; private StreamingDataRequest(String videoId, Map playerHeaders) { Objects.requireNonNull(playerHeaders); @@ -134,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, @@ -146,7 +156,10 @@ public class StreamingDataRequest { final long startTime = System.currentTimeMillis(); try { - HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); + Route.CompiledRoute route = clientType.usePlayerEndpoint ? + GET_PLAYER_STREAMING_DATA : GET_REEL_STREAMING_DATA; + + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(route, clientType); connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS); connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS); @@ -203,7 +216,7 @@ public class StreamingDataRequest { return null; } - private static ByteBuffer fetch(String videoId, Map playerHeaders) { + private static byte[] fetch(String videoId, Map playerHeaders) { final boolean debugEnabled = BaseSettings.DEBUG.get(); // Retry with different client if empty response body is received. @@ -214,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; } } } @@ -250,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) { diff --git a/extensions/shared/protobuf/build.gradle.kts b/extensions/shared/protobuf/build.gradle.kts new file mode 100644 index 0000000000..be3f7f6a08 --- /dev/null +++ b/extensions/shared/protobuf/build.gradle.kts @@ -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") { + 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) } } + diff --git a/extensions/shared/protobuf/src/main/proto/app/revanced/extension/shared/innertube/player_response.proto b/extensions/shared/protobuf/src/main/proto/app/revanced/extension/shared/innertube/player_response.proto new file mode 100644 index 0000000000..29f1a6d9bd --- /dev/null +++ b/extensions/shared/protobuf/src/main/proto/app/revanced/extension/shared/innertube/player_response.proto @@ -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; +} diff --git a/extensions/shared/protobuf/src/main/proto/app/revanced/extension/shared/innertube/reel_item_watch_response.proto b/extensions/shared/protobuf/src/main/proto/app/revanced/extension/shared/innertube/reel_item_watch_response.proto new file mode 100644 index 0000000000..e74f142533 --- /dev/null +++ b/extensions/shared/protobuf/src/main/proto/app/revanced/extension/shared/innertube/reel_item_watch_response.proto @@ -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; + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java index 2bf442a937..21819a2828 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java @@ -1,10 +1,9 @@ package app.revanced.extension.youtube.patches.spoof; import static app.revanced.extension.shared.spoof.ClientType.ANDROID_CREATOR; -import static app.revanced.extension.shared.spoof.ClientType.ANDROID_NO_SDK; import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32; import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48; -import static app.revanced.extension.shared.spoof.ClientType.IPADOS; +import static app.revanced.extension.shared.spoof.ClientType.ANDROID_REEL; import static app.revanced.extension.shared.spoof.ClientType.VISIONOS; import java.util.List; @@ -44,11 +43,11 @@ public class SpoofVideoStreamsPatch { } List availableClients = List.of( - VISIONOS, - ANDROID_CREATOR, + ANDROID_REEL, ANDROID_VR_1_43_32, - ANDROID_NO_SDK, - IPADOS); + VISIONOS, + ANDROID_CREATOR + ); app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setClientsToUse( availableClients, client); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofVideoStreamsSideEffectsPreference.java similarity index 75% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java rename to extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofVideoStreamsSideEffectsPreference.java index 86802ee200..1faaaf0a3e 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofVideoStreamsSideEffectsPreference.java @@ -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); } @@ -88,27 +88,23 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference { + '\n' + str("revanced_spoof_video_streams_about_no_stable_volume") + '\n' + str("revanced_spoof_video_streams_about_no_av1") + '\n' + str("revanced_spoof_video_streams_about_no_force_original_audio"); + case ANDROID_REEL -> + 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 ANDROID_NO_SDK -> - summary = str("revanced_spoof_video_streams_about_playback_failure"); - case IPADOS -> summary = str("revanced_spoof_video_streams_about_playback_failure") - + '\n' + str("revanced_spoof_video_streams_about_no_av1"); - case VISIONOS -> - summary = str("revanced_spoof_video_streams_about_experimental") + '\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); } - // Only iPadOS can play children videos in incognito, but it commonly fails at 1 minute - // or doesn't start playback at all. List the side effect for other clients - // since they will fall over to iPadOS. - if (clientType != ClientType.IPADOS && clientType != ClientType.ANDROID_NO_SDK) { - summary += '\n' + str("revanced_spoof_video_streams_about_kids_videos"); + // Only Android Reel and Android VR supports 360° VR immersive mode. + if (!clientType.name().startsWith("ANDROID_VR") && clientType != ClientType.ANDROID_REEL) { + summary += '\n' + str("revanced_spoof_video_streams_about_no_immersive_mode"); } // Use better formatting for bullet points. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bea84117e6..5db73fbcee 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt index 7662718fc8..1c918b8104 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt @@ -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 diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt index 980e64888e..05a8f57caf 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt @@ -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"), ), ), ) diff --git a/patches/src/main/resources/addresources/values/arrays.xml b/patches/src/main/resources/addresources/values/arrays.xml index 230b9b8f38..e75cb6671a 100644 --- a/patches/src/main/resources/addresources/values/arrays.xml +++ b/patches/src/main/resources/addresources/values/arrays.xml @@ -220,14 +220,14 @@ + Android Reel Android VR visionOS - Android No SDK + ANDROID_REEL ANDROID_VR_1_43_32 VISIONOS - ANDROID_NO_SDK @@ -264,18 +264,16 @@ + Android Reel Android VR Android Studio - Android No SDK visionOS - iPadOS + ANDROID_REEL ANDROID_VR_1_43_32 ANDROID_CREATOR - ANDROID_NO_SDK VISIONOS - IPADOS @@ -663,8 +661,7 @@ cross-out - - + @string/revanced_block_embedded_ads_entry_1 diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index 95da75bad4..28a4d71661 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -144,6 +144,16 @@ If you are a YouTube Premium user, this setting may not be required" Playback may not work" Turning off this setting may cause playback issues. Default client + + • Experimental client and may stop working anytime + • Video may stop at 1:00, or may not be available in some regions + • Audio track menu is missing + • No AV1 video codec + • 360° VR immersive mode is not available + • Stable volume is not available + Spoofing side effects + + • Force original audio is not available Force original audio language @@ -900,10 +910,10 @@ Adjust volume by swiping vertically on the right side of the screen" Audio track menu is hidden Audio track menu is shown + 'Android Reel' must be kept untranslated. --> "Audio track menu is hidden -To show the Audio track menu, change \'Spoof video streams\' to \'Android No SDK\'" +To show the Audio track menu, change \'Spoof video streams\' to \'Android >Reel\'" Hide Watch in VR Watch in VR menu is hidden @@ -1786,18 +1796,9 @@ Playback may stutter or drop frames" "Enabling this setting may use software AV1 decoding. Video playback with AV1 may stutter or drop frames." - Spoofing side effects - • Experimental client and may stop working anytime - • Video may stop at 1:00, or may not be available in some regions - • Audio track menu is missing - • No AV1 video codec - • Stable volume is not available - • Kids videos may not play when logged out or in incognito mode - - • Force original audio is not available - Show in Stats for nerds - Client type is shown in Stats for nerds - Client is hidden in Stats for nerds + Show in Stats for nerds + Client type is shown in Stats for nerds + Client is hidden in Stats for nerds diff --git a/settings.gradle.kts b/settings.gradle.kts index 167fb51c8b..1a95f2d066 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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") \ No newline at end of file