From d498d79597dd981fca477b6a4a3df9dcd1b3845a Mon Sep 17 00:00:00 2001 From: zainarbani Date: Sat, 24 Aug 2024 11:29:04 +0700 Subject: [PATCH 01/91] fix(YouTube - Spoof Client): Fix playback by replace streaming data --- .../patches/spoof/SpoofClientPatch.java | 98 +++++++++++++-- .../patches/spoof/requests/PlayerRoutes.java | 85 ++++++++++++- .../requests/StreamingDataRequester.java | 114 ++++++++++++++++++ .../youtube/settings/Settings.java | 1 + 4 files changed, 290 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 2b29dd97ce..557f438aea 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -1,12 +1,20 @@ package app.revanced.integrations.youtube.patches.spoof; +import androidx.annotation.Nullable; +import android.annotation.SuppressLint; import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.net.Uri; import android.os.Build; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.youtube.patches.BackgroundPlaybackPatch; +import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequester; import app.revanced.integrations.youtube.settings.Settings; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import org.chromium.net.ExperimentalUrlRequest; @SuppressWarnings("unused") @@ -14,6 +22,7 @@ public class SpoofClientPatch { private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get(); private static final ClientType SPOOF_CLIENT_TYPE = Settings.SPOOF_CLIENT_USE_IOS.get() ? ClientType.IOS : ClientType.ANDROID_VR; private static final boolean SPOOFING_TO_IOS = SPOOF_CLIENT_ENABLED && SPOOF_CLIENT_TYPE == ClientType.IOS; + private static final boolean SPOOF_STREAM_ENABLED = Settings.SPOOF_STREAM.get(); /** * Any unreachable ip address. Used to intentionally fail requests. @@ -21,6 +30,19 @@ public class SpoofClientPatch { private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0"; private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING); + /** + * Streaming data store. + */ + @Nullable + private static CompletableFuture streamingDataFuture; + private static final ConcurrentHashMap streamingDataCache = new ConcurrentHashMap<>(); + + /** + * Last video id prefetched. Field is to prevent prefetching the same video id multiple times in a row. + */ + @Nullable + private static volatile String lastPrefetchedVideoId; + /** * Injection point. * Blocks /get_watch requests by returning an unreachable URI. @@ -29,7 +51,7 @@ public class SpoofClientPatch { * @return An unreachable URI if the request is a /get_watch request, otherwise the original URI. */ public static Uri blockGetWatchRequest(Uri playerRequestUri) { - if (SPOOF_CLIENT_ENABLED) { + if (SPOOF_CLIENT_ENABLED || SPOOF_STREAM_ENABLED) { try { String path = playerRequestUri.getPath(); @@ -52,7 +74,7 @@ public static Uri blockGetWatchRequest(Uri playerRequestUri) { * Blocks /initplayback requests. */ public static String blockInitPlaybackRequest(String originalUrlString) { - if (SPOOF_CLIENT_ENABLED) { + if (SPOOF_CLIENT_ENABLED || SPOOF_STREAM_ENABLED) { try { var originalUri = Uri.parse(originalUrlString); String path = originalUri.getPath(); @@ -113,6 +135,13 @@ public static boolean isClientSpoofingEnabled() { return SPOOF_CLIENT_ENABLED; } + /** + * Injection point. + */ + public static boolean isSpoofStreamEnabled() { + return SPOOF_STREAM_ENABLED; + } + /** * Injection point. * When spoofing the client to iOS, the playback speed menu is missing from the player response. @@ -135,17 +164,72 @@ public static boolean overrideBackgroundAudioPlayback() { * Injection point. * Fix video qualities missing, if spoofing to iOS by using the correct iOS user-agent. */ - public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Builder builder, String url) { - if (SPOOFING_TO_IOS) { - String path = Uri.parse(url).getPath(); - if (path != null && path.contains("player")) { - return builder.addHeader("User-Agent", ClientType.IOS.userAgent).build(); + public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Builder builder, String url, Map headers) { + if (SPOOFING_TO_IOS || SPOOF_STREAM_ENABLED) { + Uri uri = Uri.parse(url); + String path = uri.getPath(); + if (path != null && path.contains("player") && !path.contains("heartbeat")) { + if (SPOOFING_TO_IOS) { + return builder.addHeader("User-Agent", ClientType.IOS.userAgent).build(); + } + if (SPOOF_STREAM_ENABLED) { + fetchStreamingData(uri.getQueryParameter("id"), headers); + return builder.build(); + } } } return builder.build(); } + /** + * Injection point. + * Fix playback by replace the streaming data. + */ + @SuppressLint("NewApi") + public static ByteBuffer getStreamingData(String videoId) { + if (!SPOOF_STREAM_ENABLED) return null; + + if (streamingDataCache.containsKey(videoId)) { + return streamingDataCache.get(videoId); + } + + if (streamingDataFuture != null) { + try { + ByteBuffer byteBuffer = streamingDataFuture.get(); + if (byteBuffer != null) { + streamingDataCache.put(videoId, byteBuffer); + return byteBuffer; + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + Logger.printException(() -> "getStreamingData interrupted.", ex); + } catch (ExecutionException ex) { + Logger.printException(() -> "getStreamingData failure.", ex); + } + } + + return null; + } + + /** + * Injection point. + */ + public static void fetchStreamingData(String videoId, Map headers) { + if (SPOOF_STREAM_ENABLED) { + if (videoId.equals(lastPrefetchedVideoId)) { + return; + } + + if (!streamingDataCache.containsKey(videoId)) { + String authHeader = (String) headers.get("Authorization"); + CompletableFuture future = StreamingDataRequester.fetch(videoId, authHeader); + streamingDataFuture = future; + } + lastPrefetchedVideoId = videoId; + } + } + private enum ClientType { // https://dumps.tadiphone.dev/dumps/oculus/eureka ANDROID_VR(28, diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java index 1927b1d68a..a12e2061d8 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java @@ -11,7 +11,8 @@ import java.net.HttpURLConnection; final class PlayerRoutes { - private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/"; + private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"; + static final Route.CompiledRoute GET_STORYBOARD_SPEC_RENDERER = new Route( Route.Method.POST, "player" + @@ -20,7 +21,17 @@ final class PlayerRoutes { "playabilityStatus.status" ).compile(); + static final Route.CompiledRoute GET_STREAMING_DATA = new Route( + Route.Method.POST, + "player" + + "?fields=streamingData" + + "&alt=proto" + ).compile(); + static final String ANDROID_INNER_TUBE_BODY; + static final String VR_INNER_TUBE_BODY; + static final String UNPLUGGED_INNER_TUBE_BODY; + static final String TESTSUITE_INNER_TUBE_BODY; static final String TV_EMBED_INNER_TUBE_BODY; /** @@ -49,6 +60,78 @@ final class PlayerRoutes { ANDROID_INNER_TUBE_BODY = innerTubeBody.toString(); + JSONObject vrInnerTubeBody = new JSONObject(); + + try { + JSONObject context = new JSONObject(); + + JSONObject client = new JSONObject(); + client.put("clientName", "ANDROID_VR"); + client.put("clientVersion", "1.58.14"); + client.put("deviceModel", "Quest 3"); + client.put("osVersion", "12"); + client.put("androidSdkVersion", 34); + + context.put("client", client); + + vrInnerTubeBody.put("contentCheckOk", true); + vrInnerTubeBody.put("racyCheckOk", true); + vrInnerTubeBody.put("context", context); + vrInnerTubeBody.put("videoId", "%s"); + } catch (JSONException e) { + Logger.printException(() -> "Failed to create vrInnerTubeBody", e); + } + + VR_INNER_TUBE_BODY = vrInnerTubeBody.toString(); + + JSONObject unpluggedInnerTubeBody = new JSONObject(); + + try { + JSONObject context = new JSONObject(); + + JSONObject client = new JSONObject(); + client.put("clientName", "ANDROID_UNPLUGGED"); + client.put("clientVersion", "8.33.0"); + client.put("deviceModel", "ADT-3"); + client.put("osVersion", "14"); + client.put("androidSdkVersion", 34); + + context.put("client", client); + + unpluggedInnerTubeBody.put("contentCheckOk", true); + unpluggedInnerTubeBody.put("racyCheckOk", true); + unpluggedInnerTubeBody.put("context", context); + unpluggedInnerTubeBody.put("videoId", "%s"); + } catch (JSONException e) { + Logger.printException(() -> "Failed to create unpluggedInnerTubeBody", e); + } + + UNPLUGGED_INNER_TUBE_BODY = unpluggedInnerTubeBody.toString(); + + JSONObject suiteInnerTubeBody = new JSONObject(); + + try { + JSONObject context = new JSONObject(); + + JSONObject client = new JSONObject(); + client.put("clientName", "ANDROID_TESTSUITE"); + client.put("clientVersion", "1.9"); + client.put("deviceModel", "Pixel 8 Pro"); + client.put("osVersion", "14"); + client.put("androidSdkVersion", 34); + + context.put("client", client); + + suiteInnerTubeBody.put("contentCheckOk", true); + suiteInnerTubeBody.put("racyCheckOk", true); + suiteInnerTubeBody.put("context", context); + suiteInnerTubeBody.put("videoId", "%s"); + } catch (JSONException e) { + Logger.printException(() -> "Failed to create suiteInnerTubeBody", e); + } + + TESTSUITE_INNER_TUBE_BODY = suiteInnerTubeBody.toString(); + JSONObject tvEmbedInnerTubeBody = new JSONObject(); try { diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java new file mode 100644 index 0000000000..51e76eb13c --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java @@ -0,0 +1,114 @@ +package app.revanced.integrations.youtube.patches.spoof.requests; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.annotation.SuppressLint; + +import app.revanced.integrations.shared.settings.BaseSettings; +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; + +import java.io.ByteArrayOutputStream; +import java.io.BufferedInputStream; +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.*; + +public class StreamingDataRequester { + private static final boolean showToastOnException = false; + + private StreamingDataRequester() { + } + + private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) { + if (showToast) Utils.showToastShort(toastMessage); + Logger.printInfo(() -> toastMessage, ex); + } + + @Nullable + private static HttpURLConnection send(String requestBody, String authHeader) { + final long startTime = System.currentTimeMillis(); + try { + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA); + + // Required for age restricted videos. + if (authHeader != null) { + connection.setRequestProperty("authorization", authHeader); + } + + final byte[] innerTubeBody = requestBody.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(innerTubeBody.length); + connection.getOutputStream().write(innerTubeBody); + + final int responseCode = connection.getResponseCode(); + if (responseCode == 200) return connection; + + handleConnectionError("Not available: " + responseCode, null, + showToastOnException || BaseSettings.DEBUG_TOAST_ON_ERROR.get()); + } catch (SocketTimeoutException ex) { + handleConnectionError("Connection timeout.", ex, showToastOnException); + } catch (IOException ex) { + handleConnectionError("Network error.", ex, showToastOnException); + } catch (Exception ex) { + Logger.printException(() -> "Request failed.", ex); + } finally { + Logger.printDebug(() -> "Took: " + (System.currentTimeMillis() - startTime) + "ms"); + } + + return null; + } + + @SuppressLint("NewApi") + public static CompletableFuture fetch(@NonNull String videoId, String authHeader) { + Objects.requireNonNull(videoId); + + return CompletableFuture.supplyAsync(() -> { + ByteBuffer finalBuffer = null; + + // Retry with different client if empty streaming data is received. + List innerTubeBodies = List.of( + VR_INNER_TUBE_BODY, + UNPLUGGED_INNER_TUBE_BODY, + TESTSUITE_INNER_TUBE_BODY + ); + + for (String body : innerTubeBodies) { + HttpURLConnection connection = send(String.format(body, videoId), authHeader); + if (connection != null) { + try { + // gzip encoding doesn't response with content length (-1), + // but empty response body does. + if (connection.getContentLength() != 0) { + InputStream inputStream = new BufferedInputStream(connection.getInputStream()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + finalBuffer = ByteBuffer.wrap(baos.toByteArray()); + break; + } + } catch (IOException ex) { + Logger.printException(() -> "Failed while processing response data.", ex); + } + } + } + + if (finalBuffer == null) { + handleConnectionError("No streaming data available.", null, showToastOnException); + } + + return finalBuffer; + }); + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 47ee02c8e2..afb3321193 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -255,6 +255,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE); public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true, "revanced_spoof_client_user_dialog_message"); public static final BooleanSetting SPOOF_CLIENT_USE_IOS = new BooleanSetting("revanced_spoof_client_use_ios", TRUE, true, parent(SPOOF_CLIENT)); + public static final BooleanSetting SPOOF_STREAM = new BooleanSetting("revanced_spoof_stream", FALSE, true); @Deprecated public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", ""); public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1); From 109a83e3483e668455fbfa7172a3ccb3568d5ab7 Mon Sep 17 00:00:00 2001 From: Zain Date: Sat, 24 Aug 2024 19:25:47 +0700 Subject: [PATCH 02/91] Fix missmerge --- .../integrations/youtube/patches/spoof/SpoofClientPatch.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 8a4ddf88c6..1cbc55c130 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -177,7 +177,7 @@ public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Bu String path = uri.getPath(); if (path != null && path.contains("player") && !path.contains("heartbeat")) { if (SPOOF_CLIENT_ENABLED) { - return builder.addHeader("User-Agent", ClientType.IOS.userAgent).build(); + return builder.addHeader("User-Agent", SPOOF_CLIENT_TYPE.userAgent).build(); } if (SPOOF_STREAM_ENABLED) { fetchStreamingData(uri.getQueryParameter("id"), headers); From 0bc88bdb8b740b028a3670655e4f2903741eea41 Mon Sep 17 00:00:00 2001 From: zainarbani Date: Sun, 25 Aug 2024 18:25:14 +0700 Subject: [PATCH 03/91] Add iOS client as streaming data source --- .../patches/spoof/SpoofClientPatch.java | 48 ++++-- .../patches/spoof/requests/PlayerRoutes.java | 138 +++--------------- .../requests/StoryboardRendererRequester.java | 33 +++-- .../requests/StreamingDataRequester.java | 36 ++--- 4 files changed, 93 insertions(+), 162 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 1cbc55c130..5762226597 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -19,6 +19,7 @@ import org.chromium.net.ExperimentalUrlRequest; import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; import app.revanced.integrations.shared.settings.Setting; import app.revanced.integrations.youtube.patches.BackgroundPlaybackPatch; import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequester; @@ -40,9 +41,9 @@ public class SpoofClientPatch { /** * Streaming data store. */ - @Nullable - private static CompletableFuture streamingDataFuture; - private static final ConcurrentHashMap streamingDataCache = new ConcurrentHashMap<>(); + @Nullable + private static CompletableFuture streamingDataFuture; + private static final ConcurrentHashMap streamingDataCache = new ConcurrentHashMap<>(); /** * Last video id prefetched. Field is to prevent prefetching the same video id multiple times in a row. @@ -171,7 +172,7 @@ public static boolean overrideBackgroundAudioPlayback() { * Injection point. * Fix video qualities missing, if spoofing to iOS by using the correct iOS user-agent. */ - public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Builder builder, String url, Map headers) { + public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Builder builder, String url, Map playerHeaders) { if (SPOOF_CLIENT_ENABLED || SPOOF_STREAM_ENABLED) { Uri uri = Uri.parse(url); String path = uri.getPath(); @@ -180,7 +181,7 @@ public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Bu return builder.addHeader("User-Agent", SPOOF_CLIENT_TYPE.userAgent).build(); } if (SPOOF_STREAM_ENABLED) { - fetchStreamingData(uri.getQueryParameter("id"), headers); + fetchStreamingData(uri.getQueryParameter("id"), playerHeaders); return builder.build(); } } @@ -222,21 +223,35 @@ public static ByteBuffer getStreamingData(String videoId) { /** * Injection point. */ - public static void fetchStreamingData(String videoId, Map headers) { + public static void fetchStreamingData(String videoId, Map playerHeaders) { if (SPOOF_STREAM_ENABLED) { if (videoId.equals(lastPrefetchedVideoId)) { return; } if (!streamingDataCache.containsKey(videoId)) { - String authHeader = (String) headers.get("Authorization"); - CompletableFuture future = StreamingDataRequester.fetch(videoId, authHeader); + CompletableFuture future = StreamingDataRequester.fetch(videoId, playerHeaders); streamingDataFuture = future; } lastPrefetchedVideoId = videoId; } } + /** + * Injection point. + */ + public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) { + if (!SPOOF_STREAM_ENABLED) return postData; + + String path = uri.getPath(); + boolean iosClient = ClientType.IOS.name().equals(uri.getQueryParameter("c")); + if (path != null && path.contains("videoplayback") && method == 2 && iosClient) { + return null; + } + + return postData; + } + // Must check for device features in a separate class and cannot place this code inside // the Patch or ClientType enum due to cyclic Setting references. static class DeviceHardwareSupport { @@ -318,33 +333,40 @@ public enum ClientType { "12", "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", "1.56.21" + ), + ANDROID(3, + Build.MODEL, + Build.VERSION.RELEASE, + String.format("com.google.android.youtube/%s (Linux; U; Android %s; GB) gzip", + Utils.getAppVersionName(), Build.VERSION.RELEASE), + Utils.getAppVersionName() ); /** * YouTube * client type */ - final int id; + public final int id; /** * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) */ - final String model; + public final String model; /** * Device OS version. */ - final String osVersion; + public final String osVersion; /** * Player user-agent. */ - final String userAgent; + public final String userAgent; /** * App version. */ - final String appVersion; + public final String appVersion; ClientType(int id, String model, String osVersion, String userAgent, String appVersion) { this.id = id; diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java index a12e2061d8..fd0977d8a5 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java @@ -1,9 +1,9 @@ package app.revanced.integrations.youtube.patches.spoof.requests; +import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; import app.revanced.integrations.youtube.requests.Requester; import app.revanced.integrations.youtube.requests.Route; import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; import org.json.JSONException; import org.json.JSONObject; @@ -27,151 +27,49 @@ final class PlayerRoutes { "?fields=streamingData" + "&alt=proto" ).compile(); - - static final String ANDROID_INNER_TUBE_BODY; - static final String VR_INNER_TUBE_BODY; - static final String UNPLUGGED_INNER_TUBE_BODY; - static final String TESTSUITE_INNER_TUBE_BODY; - static final String TV_EMBED_INNER_TUBE_BODY; /** * TCP connection and HTTP read timeout */ private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds. - static { - JSONObject innerTubeBody = new JSONObject(); + private PlayerRoutes() { + } + + static String createInnertubeBody(ClientType clientType) { + JSONObject innerTubeBody = new JSONObject(); try { JSONObject context = new JSONObject(); JSONObject client = new JSONObject(); - client.put("clientName", "ANDROID"); - client.put("clientVersion", Utils.getAppVersionName()); - client.put("androidSdkVersion", 34); + client.put("clientName", clientType.name()); + client.put("clientVersion", clientType.appVersion); + client.put("deviceModel", clientType.model); + client.put("osVersion", clientType.osVersion); + if (clientType != ClientType.IOS) { + client.put("androidSdkVersion", 34); + } context.put("client", client); innerTubeBody.put("context", context); + innerTubeBody.put("contentCheckOk", true); + innerTubeBody.put("racyCheckOk", true); innerTubeBody.put("videoId", "%s"); } catch (JSONException e) { Logger.printException(() -> "Failed to create innerTubeBody", e); } - ANDROID_INNER_TUBE_BODY = innerTubeBody.toString(); - - JSONObject vrInnerTubeBody = new JSONObject(); - - try { - JSONObject context = new JSONObject(); - - JSONObject client = new JSONObject(); - client.put("clientName", "ANDROID_VR"); - client.put("clientVersion", "1.58.14"); - client.put("deviceModel", "Quest 3"); - client.put("osVersion", "12"); - client.put("androidSdkVersion", 34); - - context.put("client", client); - - vrInnerTubeBody.put("contentCheckOk", true); - vrInnerTubeBody.put("racyCheckOk", true); - vrInnerTubeBody.put("context", context); - vrInnerTubeBody.put("videoId", "%s"); - } catch (JSONException e) { - Logger.printException(() -> "Failed to create vrInnerTubeBody", e); - } - - VR_INNER_TUBE_BODY = vrInnerTubeBody.toString(); - - JSONObject unpluggedInnerTubeBody = new JSONObject(); - - try { - JSONObject context = new JSONObject(); - - JSONObject client = new JSONObject(); - client.put("clientName", "ANDROID_UNPLUGGED"); - client.put("clientVersion", "8.33.0"); - client.put("deviceModel", "ADT-3"); - client.put("osVersion", "14"); - client.put("androidSdkVersion", 34); - - context.put("client", client); - - unpluggedInnerTubeBody.put("contentCheckOk", true); - unpluggedInnerTubeBody.put("racyCheckOk", true); - unpluggedInnerTubeBody.put("context", context); - unpluggedInnerTubeBody.put("videoId", "%s"); - } catch (JSONException e) { - Logger.printException(() -> "Failed to create unpluggedInnerTubeBody", e); - } - - UNPLUGGED_INNER_TUBE_BODY = unpluggedInnerTubeBody.toString(); - - JSONObject suiteInnerTubeBody = new JSONObject(); - - try { - JSONObject context = new JSONObject(); - - JSONObject client = new JSONObject(); - client.put("clientName", "ANDROID_TESTSUITE"); - client.put("clientVersion", "1.9"); - client.put("deviceModel", "Pixel 8 Pro"); - client.put("osVersion", "14"); - client.put("androidSdkVersion", 34); - - context.put("client", client); - - suiteInnerTubeBody.put("contentCheckOk", true); - suiteInnerTubeBody.put("racyCheckOk", true); - suiteInnerTubeBody.put("context", context); - suiteInnerTubeBody.put("videoId", "%s"); - } catch (JSONException e) { - Logger.printException(() -> "Failed to create suiteInnerTubeBody", e); - } - - TESTSUITE_INNER_TUBE_BODY = suiteInnerTubeBody.toString(); - - JSONObject tvEmbedInnerTubeBody = new JSONObject(); - - try { - JSONObject context = new JSONObject(); - - JSONObject client = new JSONObject(); - client.put("clientName", "TVHTML5_SIMPLY_EMBEDDED_PLAYER"); - client.put("clientVersion", "2.0"); - client.put("platform", "TV"); - client.put("clientScreen", "EMBED"); - - JSONObject thirdParty = new JSONObject(); - thirdParty.put("embedUrl", "https://www.youtube.com/watch?v=%s"); - - context.put("thirdParty", thirdParty); - context.put("client", client); - - tvEmbedInnerTubeBody.put("context", context); - tvEmbedInnerTubeBody.put("videoId", "%s"); - } catch (JSONException e) { - Logger.printException(() -> "Failed to create tvEmbedInnerTubeBody", e); - } - - TV_EMBED_INNER_TUBE_BODY = tvEmbedInnerTubeBody.toString(); - } - - private PlayerRoutes() { + return innerTubeBody.toString(); } /** @noinspection SameParameterValue*/ - static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route) throws IOException { + static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException { var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route); - connection.setRequestProperty( - "User-Agent", "com.google.android.youtube/" + - Utils.getAppVersionName() + - " (Linux; U; Android 12; GB) gzip" - ); - connection.setRequestProperty("X-Goog-Api-Format-Version", "2"); connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("User-Agent", clientType.userAgent); connection.setUseCaches(false); connection.setDoOutput(true); diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java index 0cbec19400..2fd9be79eb 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java @@ -4,6 +4,7 @@ import androidx.annotation.Nullable; import app.revanced.integrations.shared.settings.BaseSettings; import app.revanced.integrations.youtube.patches.spoof.StoryboardRenderer; +import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; import app.revanced.integrations.youtube.requests.Requester; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; @@ -15,6 +16,7 @@ import java.net.SocketTimeoutException; import java.nio.charset.StandardCharsets; import java.util.Objects; +import java.util.List; import static app.revanced.integrations.shared.StringRef.str; import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.*; @@ -39,7 +41,7 @@ private static void handleConnectionError(@NonNull String toastMessage, @Nullabl } @Nullable - private static JSONObject fetchPlayerResponse(@NonNull String requestBody, boolean showToastOnIOException) { + private static JSONObject fetchPlayerResponse(@NonNull String requestBody, ClientType clientType, boolean showToastOnIOException) { final long startTime = System.currentTimeMillis(); try { Utils.verifyOffMainThread(); @@ -47,7 +49,8 @@ private static JSONObject fetchPlayerResponse(@NonNull String requestBody, boole final byte[] innerTubeBody = requestBody.getBytes(StandardCharsets.UTF_8); - HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STORYBOARD_SPEC_RENDERER); + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STORYBOARD_SPEC_RENDERER, clientType); + connection.setRequestProperty("User-Agent", ClientType.ANDROID.userAgent); connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length); final int responseCode = connection.getResponseCode(); @@ -90,9 +93,10 @@ private static boolean isPlayabilityStatusOk(@NonNull JSONObject playerResponse) */ @Nullable private static StoryboardRenderer getStoryboardRendererUsingBody(String videoId, - @NonNull String innerTubeBody, + ClientType clientType, boolean showToastOnIOException) { - final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody, showToastOnIOException); + String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); + final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody, clientType, showToastOnIOException); if (playerResponse != null && isPlayabilityStatusOk(playerResponse)) return getStoryboardRendererUsingResponse(videoId, playerResponse); @@ -137,15 +141,20 @@ private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull St public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) { Objects.requireNonNull(videoId); - var renderer = getStoryboardRendererUsingBody(videoId, - String.format(ANDROID_INNER_TUBE_BODY, videoId), false); + StoryboardRenderer renderer = null; + + List clientTypeList = List.of( + ClientType.ANDROID, + ClientType.ANDROID_VR + ); + + for (ClientType clientType : clientTypeList) { + renderer = getStoryboardRendererUsingBody(videoId, clientType, false); + if (renderer != null) break; + } + if (renderer == null) { - Logger.printDebug(() -> videoId + " not available using Android client"); - renderer = getStoryboardRendererUsingBody(videoId, - String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId), true); - if (renderer == null) { - Logger.printDebug(() -> videoId + " not available using TV embedded client"); - } + Logger.printDebug(() -> videoId + " not available"); } return renderer; diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java index 51e76eb13c..4fbba0a9b7 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java @@ -7,6 +7,7 @@ import app.revanced.integrations.shared.settings.BaseSettings; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; import java.io.ByteArrayOutputStream; import java.io.BufferedInputStream; @@ -17,6 +18,7 @@ import java.net.SocketTimeoutException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.util.Map; import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -35,19 +37,20 @@ private static void handleConnectionError(String toastMessage, @Nullable Excepti } @Nullable - private static HttpURLConnection send(String requestBody, String authHeader) { + private static HttpURLConnection send(ClientType clientType, String videoId, Map playerHeaders) { final long startTime = System.currentTimeMillis(); try { - HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA); + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); - // Required for age restricted videos. - if (authHeader != null) { - connection.setRequestProperty("authorization", authHeader); - } + final String authHeader = (String) playerHeaders.get("Authorization"); + final String visitorId = (String) playerHeaders.get("X-Goog-Visitor-Id"); + connection.setRequestProperty("Authorization", authHeader); + connection.setRequestProperty("X-Goog-Visitor-Id", visitorId); - final byte[] innerTubeBody = requestBody.getBytes(StandardCharsets.UTF_8); - connection.setFixedLengthStreamingMode(innerTubeBody.length); - connection.getOutputStream().write(innerTubeBody); + final String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); + final byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(requestBody.length); + connection.getOutputStream().write(requestBody); final int responseCode = connection.getResponseCode(); if (responseCode == 200) return connection; @@ -68,21 +71,20 @@ private static HttpURLConnection send(String requestBody, String authHeader) { } @SuppressLint("NewApi") - public static CompletableFuture fetch(@NonNull String videoId, String authHeader) { + public static CompletableFuture fetch(@NonNull String videoId, Map playerHeaders) { Objects.requireNonNull(videoId); return CompletableFuture.supplyAsync(() -> { ByteBuffer finalBuffer = null; - // Retry with different client if empty streaming data is received. - List innerTubeBodies = List.of( - VR_INNER_TUBE_BODY, - UNPLUGGED_INNER_TUBE_BODY, - TESTSUITE_INNER_TUBE_BODY + // Retry with different client if empty response body is received. + List clientTypeList = List.of( + ClientType.IOS, + ClientType.ANDROID_VR ); - for (String body : innerTubeBodies) { - HttpURLConnection connection = send(String.format(body, videoId), authHeader); + for (ClientType clientType : clientTypeList) { + HttpURLConnection connection = send(clientType, videoId, playerHeaders); if (connection != null) { try { // gzip encoding doesn't response with content length (-1), From 38a54d1299bdb11098a3c14b34aadfea258329ea Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 26 Aug 2024 00:27:25 -0400 Subject: [PATCH 04/91] refactor A stream cache does not appear to be needed, as YT seems to be caching the streams when swiping thru Shorts. Streams have authentication time limits as well so it's easier and more reliable to just fetch again if the same video is closed then reopened. Can use a regular Future as none of the features of CompletableFuture is being used here. Using an executor future also avoids any potential StrictMode policy violations if the calling YT thread is set to never make network calls. --- .../patches/spoof/SpoofClientPatch.java | 145 ++++++++---------- .../requests/StoryboardRendererRequester.java | 2 +- .../requests/StreamingDataRequester.java | 88 +++++------ 3 files changed, 110 insertions(+), 125 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 5762226597..8055e5f493 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -3,20 +3,21 @@ import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.DeviceHardwareSupport.allowAV1; import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.DeviceHardwareSupport.allowVP9; -import androidx.annotation.Nullable; -import android.annotation.SuppressLint; import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.net.Uri; import android.os.Build; +import androidx.annotation.Nullable; + +import org.chromium.net.ExperimentalUrlRequest; + import java.nio.ByteBuffer; import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; - -import org.chromium.net.ExperimentalUrlRequest; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; @@ -27,10 +28,11 @@ @SuppressWarnings("unused") public class SpoofClientPatch { - private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get(); + private static final boolean SPOOF_CLIENT = Settings.SPOOF_CLIENT.get(); private static final ClientType SPOOF_CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get(); - private static final boolean SPOOF_IOS = SPOOF_CLIENT_ENABLED && SPOOF_CLIENT_TYPE == ClientType.IOS; - private static final boolean SPOOF_STREAM_ENABLED = Settings.SPOOF_STREAM.get(); + private static final boolean SPOOF_IOS = SPOOF_CLIENT && SPOOF_CLIENT_TYPE == ClientType.IOS; + + private static final boolean SPOOF_STREAM = Settings.SPOOF_STREAM.get(); /** * Any unreachable ip address. Used to intentionally fail requests. @@ -38,18 +40,7 @@ public class SpoofClientPatch { private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0"; private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING); - /** - * Streaming data store. - */ - @Nullable - private static CompletableFuture streamingDataFuture; - private static final ConcurrentHashMap streamingDataCache = new ConcurrentHashMap<>(); - - /** - * Last video id prefetched. Field is to prevent prefetching the same video id multiple times in a row. - */ - @Nullable - private static volatile String lastPrefetchedVideoId; + private static volatile Future currentVideoStream; /** * Injection point. @@ -59,7 +50,7 @@ public class SpoofClientPatch { * @return An unreachable URI if the request is a /get_watch request, otherwise the original URI. */ public static Uri blockGetWatchRequest(Uri playerRequestUri) { - if (SPOOF_CLIENT_ENABLED || SPOOF_STREAM_ENABLED) { + if (SPOOF_CLIENT || SPOOF_STREAM) { try { String path = playerRequestUri.getPath(); @@ -82,7 +73,7 @@ public static Uri blockGetWatchRequest(Uri playerRequestUri) { * Blocks /initplayback requests. */ public static String blockInitPlaybackRequest(String originalUrlString) { - if (SPOOF_CLIENT_ENABLED || SPOOF_STREAM_ENABLED) { + if (SPOOF_CLIENT || SPOOF_STREAM) { try { var originalUri = Uri.parse(originalUrlString); String path = originalUri.getPath(); @@ -104,21 +95,21 @@ public static String blockInitPlaybackRequest(String originalUrlString) { * Injection point. */ public static int getClientTypeId(int originalClientTypeId) { - return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.id : originalClientTypeId; + return SPOOF_CLIENT ? SPOOF_CLIENT_TYPE.id : originalClientTypeId; } /** * Injection point. */ public static String getClientVersion(String originalClientVersion) { - return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.appVersion : originalClientVersion; + return SPOOF_CLIENT ? SPOOF_CLIENT_TYPE.appVersion : originalClientVersion; } /** * Injection point. */ public static String getClientModel(String originalClientModel) { - return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.model : originalClientModel; + return SPOOF_CLIENT ? SPOOF_CLIENT_TYPE.model : originalClientModel; } /** @@ -126,28 +117,28 @@ public static String getClientModel(String originalClientModel) { * Fix video qualities missing, if spoofing to iOS by using the correct client OS version. */ public static String getOsVersion(String originalOsVersion) { - return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.osVersion : originalOsVersion; + return SPOOF_CLIENT ? SPOOF_CLIENT_TYPE.osVersion : originalOsVersion; } /** * Injection point. */ public static boolean enablePlayerGesture(boolean original) { - return SPOOF_CLIENT_ENABLED || original; + return SPOOF_CLIENT || original; } /** * Injection point. */ public static boolean isClientSpoofingEnabled() { - return SPOOF_CLIENT_ENABLED; + return SPOOF_CLIENT; } /** * Injection point. */ public static boolean isSpoofStreamEnabled() { - return SPOOF_STREAM_ENABLED; + return SPOOF_STREAM; } /** @@ -170,19 +161,19 @@ public static boolean overrideBackgroundAudioPlayback() { /** * Injection point. - * Fix video qualities missing, if spoofing to iOS by using the correct iOS user-agent. */ - public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Builder builder, String url, Map playerHeaders) { - if (SPOOF_CLIENT_ENABLED || SPOOF_STREAM_ENABLED) { + public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Builder builder, + String url, Map playerHeaders) { + if (SPOOF_CLIENT || SPOOF_STREAM) { Uri uri = Uri.parse(url); String path = uri.getPath(); if (path != null && path.contains("player") && !path.contains("heartbeat")) { - if (SPOOF_CLIENT_ENABLED) { - return builder.addHeader("User-Agent", SPOOF_CLIENT_TYPE.userAgent).build(); - } - if (SPOOF_STREAM_ENABLED) { - fetchStreamingData(uri.getQueryParameter("id"), playerHeaders); - return builder.build(); + if (SPOOF_CLIENT) { + Logger.printDebug(() -> "Overriding user agent for /player call"); + builder.addHeader("User-Agent", SPOOF_CLIENT_TYPE.userAgent); + } else { + String videoId = uri.getQueryParameter("id"); + currentVideoStream = StreamingDataRequester.fetch(videoId, playerHeaders); } } } @@ -193,27 +184,32 @@ public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Bu /** * Injection point. * Fix playback by replace the streaming data. + * Called after {@link #overrideUserAgent(ExperimentalUrlRequest.Builder, String, Map)}. */ - @SuppressLint("NewApi") + @Nullable public static ByteBuffer getStreamingData(String videoId) { - if (!SPOOF_STREAM_ENABLED) return null; - - if (streamingDataCache.containsKey(videoId)) { - return streamingDataCache.get(videoId); - } - - if (streamingDataFuture != null) { + if (SPOOF_STREAM) { try { - ByteBuffer byteBuffer = streamingDataFuture.get(); - if (byteBuffer != null) { - streamingDataCache.put(videoId, byteBuffer); - return byteBuffer; + Utils.verifyOffMainThread(); + + var future = currentVideoStream; + if (future != null) { + final long maxTimeToWait = 4000; + var stream = future.get(maxTimeToWait, TimeUnit.MILLISECONDS); + if (stream != null) { + Logger.printDebug(() -> "Overriding video stream"); + return stream; + } + + Logger.printDebug(() -> "Not overriding streaming data (video stream is null)"); } + } catch (TimeoutException ex) { + Logger.printInfo(() -> "getStreamingData timed out", ex); } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - Logger.printException(() -> "getStreamingData interrupted.", ex); + Logger.printException(() -> "getStreamingData interrupted", ex); + Thread.currentThread().interrupt(); // Restore interrupt status flag. } catch (ExecutionException ex) { - Logger.printException(() -> "getStreamingData failure.", ex); + Logger.printException(() -> "getStreamingData failure", ex); } } @@ -222,31 +218,23 @@ public static ByteBuffer getStreamingData(String videoId) { /** * Injection point. - */ - public static void fetchStreamingData(String videoId, Map playerHeaders) { - if (SPOOF_STREAM_ENABLED) { - if (videoId.equals(lastPrefetchedVideoId)) { - return; - } - - if (!streamingDataCache.containsKey(videoId)) { - CompletableFuture future = StreamingDataRequester.fetch(videoId, playerHeaders); - streamingDataFuture = future; - } - lastPrefetchedVideoId = videoId; - } - } - - /** - * Injection point. + * Called after {@link #getStreamingData(String)}. */ public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) { - if (!SPOOF_STREAM_ENABLED) return postData; - - String path = uri.getPath(); - boolean iosClient = ClientType.IOS.name().equals(uri.getQueryParameter("c")); - if (path != null && path.contains("videoplayback") && method == 2 && iosClient) { - return null; + if (SPOOF_STREAM) { + try { + final int FIXME_RENAME_THIS_TO_DESCRIBE_THIS_MAGIC_NUMBER = 2; // FIXME + if (method == FIXME_RENAME_THIS_TO_DESCRIBE_THIS_MAGIC_NUMBER) { + String path = uri.getPath(); + String FIXME_RENAME_THIS_TO_DESCRIBE_THIS_MAGIC_STRING = "c"; // FIXME + final boolean iosClient = ClientType.IOS.name().equals(uri.getQueryParameter(FIXME_RENAME_THIS_TO_DESCRIBE_THIS_MAGIC_STRING)); + if (iosClient && path != null && path.contains("videoplayback")) { + return null; + } + } + } catch (Exception ex) { + Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex); + } } return postData; @@ -380,7 +368,8 @@ public enum ClientType { public static final class ForceiOSAVCAvailability implements Setting.Availability { @Override public boolean isAvailable() { - return Settings.SPOOF_CLIENT.get() && Settings.SPOOF_CLIENT_TYPE.get() == ClientType.IOS; + return (Settings.SPOOF_CLIENT.get() && Settings.SPOOF_CLIENT_TYPE.get() == ClientType.IOS) + || Settings.SPOOF_STREAM.get(); } } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java index 2fd9be79eb..200d5fb0d1 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java @@ -21,6 +21,7 @@ import static app.revanced.integrations.shared.StringRef.str; import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.*; +@Deprecated public class StoryboardRendererRequester { private StoryboardRendererRequester() { @@ -88,7 +89,6 @@ private static boolean isPlayabilityStatusOk(@NonNull JSONObject playerResponse) /** * Fetches the storyboardRenderer from the innerTubeBody. - * @param innerTubeBody The innerTubeBody to use to fetch the storyboardRenderer. * @return StoryboardRenderer or null if playabilityStatus is not OK. */ @Nullable diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java index 4fbba0a9b7..e00b333006 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java @@ -1,32 +1,28 @@ package app.revanced.integrations.youtube.patches.spoof.requests; +import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.annotation.SuppressLint; - -import app.revanced.integrations.shared.settings.BaseSettings; -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; -import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; -import java.io.ByteArrayOutputStream; -import java.io.BufferedInputStream; import java.io.BufferedInputStream; -import java.io.InputStream; +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.Map; -import java.util.List; import java.util.Objects; -import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; -import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.*; +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.shared.settings.BaseSettings; +import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; public class StreamingDataRequester { - private static final boolean showToastOnException = false; private StreamingDataRequester() { } @@ -37,80 +33,80 @@ private static void handleConnectionError(String toastMessage, @Nullable Excepti } @Nullable - private static HttpURLConnection send(ClientType clientType, String videoId, Map playerHeaders) { + private static HttpURLConnection send(ClientType clientType, String videoId, Map playerHeaders) { final long startTime = System.currentTimeMillis(); + String clientTypeName = clientType.name(); + // Only show toast for each attempt if debug mode is enabled, + // as the calling code shows a toast if all stream calls fail. + final boolean showErrorToasts = BaseSettings.DEBUG.get(); + Logger.printDebug(() -> "Fetching video streams using client: " + clientType.name()); + try { HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); - final String authHeader = (String) playerHeaders.get("Authorization"); - final String visitorId = (String) playerHeaders.get("X-Goog-Visitor-Id"); + String authHeader = playerHeaders.get("Authorization"); + String visitorId = playerHeaders.get("X-Goog-Visitor-Id"); connection.setRequestProperty("Authorization", authHeader); connection.setRequestProperty("X-Goog-Visitor-Id", visitorId); - final String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); - final byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); + String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); + byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); connection.setFixedLengthStreamingMode(requestBody.length); connection.getOutputStream().write(requestBody); final int responseCode = connection.getResponseCode(); if (responseCode == 200) return connection; - handleConnectionError("Not available: " + responseCode, null, - showToastOnException || BaseSettings.DEBUG_TOAST_ON_ERROR.get()); + handleConnectionError(clientTypeName + " not available with response code: " + responseCode, + null, showErrorToasts); } catch (SocketTimeoutException ex) { - handleConnectionError("Connection timeout.", ex, showToastOnException); + handleConnectionError("Connection timeout", ex, showErrorToasts); } catch (IOException ex) { - handleConnectionError("Network error.", ex, showToastOnException); + handleConnectionError("Network error", ex, showErrorToasts); } catch (Exception ex) { - Logger.printException(() -> "Request failed.", ex); + Logger.printException(() -> "send failed", ex); } finally { - Logger.printDebug(() -> "Took: " + (System.currentTimeMillis() - startTime) + "ms"); + Logger.printDebug(() -> clientTypeName + " took: " + (System.currentTimeMillis() - startTime) + "ms"); } return null; } - @SuppressLint("NewApi") - public static CompletableFuture fetch(@NonNull String videoId, Map playerHeaders) { + public static Future fetch(@NonNull String videoId, Map playerHeaders) { Objects.requireNonNull(videoId); - return CompletableFuture.supplyAsync(() -> { - ByteBuffer finalBuffer = null; - + return Utils.submitOnBackgroundThread(() -> { // Retry with different client if empty response body is received. - List clientTypeList = List.of( + ClientType[] clientTypesToUse = { ClientType.IOS, ClientType.ANDROID_VR - ); + }; - for (ClientType clientType : clientTypeList) { + for (ClientType clientType : clientTypesToUse) { HttpURLConnection connection = send(clientType, videoId, playerHeaders); if (connection != null) { try { // gzip encoding doesn't response with content length (-1), // but empty response body does. if (connection.getContentLength() != 0) { - InputStream inputStream = new BufferedInputStream(connection.getInputStream()); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buffer = new byte[8192]; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) != -1) { - baos.write(buffer, 0, bytesRead); + try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + return ByteBuffer.wrap(baos.toByteArray()); } - finalBuffer = ByteBuffer.wrap(baos.toByteArray()); - break; } } catch (IOException ex) { - Logger.printException(() -> "Failed while processing response data.", ex); + Logger.printException(() -> "Fetch failed while processing response data", ex); } } } - if (finalBuffer == null) { - handleConnectionError("No streaming data available.", null, showToastOnException); - } - - return finalBuffer; + handleConnectionError("Fetch client spoof streams failed", null, true); + return null; }); } } From 3c07c12f437828ddc227b21399fdd4578ee50608 Mon Sep 17 00:00:00 2001 From: Zain Date: Mon, 26 Aug 2024 12:07:39 +0700 Subject: [PATCH 05/91] Update desc --- .../youtube/patches/spoof/SpoofClientPatch.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 8055e5f493..e8746c18da 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -223,11 +223,11 @@ public static ByteBuffer getStreamingData(String videoId) { public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) { if (SPOOF_STREAM) { try { - final int FIXME_RENAME_THIS_TO_DESCRIBE_THIS_MAGIC_NUMBER = 2; // FIXME - if (method == FIXME_RENAME_THIS_TO_DESCRIBE_THIS_MAGIC_NUMBER) { + final int methodPost = 2; + if (method == methodPost) { String path = uri.getPath(); - String FIXME_RENAME_THIS_TO_DESCRIBE_THIS_MAGIC_STRING = "c"; // FIXME - final boolean iosClient = ClientType.IOS.name().equals(uri.getQueryParameter(FIXME_RENAME_THIS_TO_DESCRIBE_THIS_MAGIC_STRING)); + String clientName = "c"; + final boolean iosClient = ClientType.IOS.name().equals(uri.getQueryParameter(clientName)); if (iosClient && path != null && path.contains("videoplayback")) { return null; } From 06824a8cfeb427036bda3b1b85259f1873bf9ad0 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 26 Aug 2024 01:24:35 -0400 Subject: [PATCH 06/91] refactor: Present stream replacement in the settings as an option similar to `iOS` and `Android VR` --- .../youtube/patches/spoof/ClientType.java | 77 +++++++ .../patches/spoof/DeviceHardwareSupport.java | 63 ++++++ .../patches/spoof/SpoofClientPatch.java | 214 +++++------------- .../patches/spoof/requests/PlayerRoutes.java | 2 +- .../requests/StoryboardRendererRequester.java | 2 +- .../requests/StreamingDataRequester.java | 2 +- .../youtube/settings/Settings.java | 8 +- 7 files changed, 205 insertions(+), 163 deletions(-) create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java new file mode 100644 index 0000000000..e99d5dc6b8 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java @@ -0,0 +1,77 @@ +package app.revanced.integrations.youtube.patches.spoof; + +import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.allowAV1; +import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.allowVP9; + +import android.os.Build; + +import app.revanced.integrations.shared.Utils; + +public enum ClientType { + // https://dumps.tadiphone.dev/dumps/oculus/eureka + IOS(5, + // iPhone 15 supports AV1 hardware decoding. + // Only use if this Android device also has hardware decoding. + allowAV1() + ? "iPhone16,2" // 15 Pro Max + : "iPhone11,4", // XS Max + // iOS 14+ forces VP9. + allowVP9() + ? "17.5.1.21F90" + : "13.7.17H35", + allowVP9() + ? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)" + : "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)", + // Version number should be a valid iOS release. + // https://www.ipa4fun.com/history/185230 + "19.10.7" + ), + ANDROID_VR(28, + "Quest 3", + "12", + "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", + "1.56.21" + ), + @Deprecated() // Android spoofing in this context no longer works. + ANDROID(3, + Build.MODEL, + Build.VERSION.RELEASE, + String.format("com.google.android.youtube/%s (Linux; U; Android %s; GB) gzip", + Utils.getAppVersionName(), Build.VERSION.RELEASE), + Utils.getAppVersionName() + ); + + /** + * YouTube + * client type + */ + public final int id; + + /** + * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) + */ + public final String model; + + /** + * Device OS version. + */ + public final String osVersion; + + /** + * Player user-agent. + */ + public final String userAgent; + + /** + * App version. + */ + public final String appVersion; + + ClientType(int id, String model, String osVersion, String userAgent, String appVersion) { + this.id = id; + this.model = model; + this.osVersion = osVersion; + this.userAgent = userAgent; + this.appVersion = appVersion; + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java new file mode 100644 index 0000000000..3d2cb7911c --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java @@ -0,0 +1,63 @@ +package app.revanced.integrations.youtube.patches.spoof; + +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.os.Build; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.youtube.settings.Settings; + +public class DeviceHardwareSupport { + private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding(); + private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding(); + + private static boolean deviceHasVP9HardwareDecoding() { + MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + + for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { + final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + ? codecInfo.isHardwareAccelerated() + : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. + if (isHardwareAccelerated && !codecInfo.isEncoder()) { + for (String type : codecInfo.getSupportedTypes()) { + if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { + Logger.printDebug(() -> "Device supports VP9 hardware decoding."); + return true; + } + } + } + } + + Logger.printDebug(() -> "Device does not support VP9 hardware decoding."); + return false; + } + + private static boolean deviceHasAV1HardwareDecoding() { + // It appears all devices with hardware AV1 are also Android 10 or newer. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + + for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { + if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) { + for (String type : codecInfo.getSupportedTypes()) { + if (type.equalsIgnoreCase("video/av01")) { + Logger.printDebug(() -> "Device supports AV1 hardware decoding."); + return true; + } + } + } + } + } + + Logger.printDebug(() -> "Device does not support AV1 hardware decoding."); + return false; + } + + public static boolean allowVP9() { + return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_CLIENT_FORCE_AVC.get(); + } + + public static boolean allowAV1() { + return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1; + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index e8746c18da..8a3603d3e9 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -1,12 +1,6 @@ package app.revanced.integrations.youtube.patches.spoof; -import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.DeviceHardwareSupport.allowAV1; -import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.DeviceHardwareSupport.allowVP9; - -import android.media.MediaCodecInfo; -import android.media.MediaCodecList; import android.net.Uri; -import android.os.Build; import androidx.annotation.Nullable; @@ -29,10 +23,10 @@ @SuppressWarnings("unused") public class SpoofClientPatch { private static final boolean SPOOF_CLIENT = Settings.SPOOF_CLIENT.get(); - private static final ClientType SPOOF_CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get(); - private static final boolean SPOOF_IOS = SPOOF_CLIENT && SPOOF_CLIENT_TYPE == ClientType.IOS; - - private static final boolean SPOOF_STREAM = Settings.SPOOF_STREAM.get(); + private static final SpoofClientStrategy SPOOF_STRATEGY = Settings.SPOOF_CLIENT_STRATEGY.get(); + @Nullable + private static final ClientType SPOOF_CLIENT_TYPE = SPOOF_CLIENT ? SPOOF_STRATEGY.clientType : null; + private static final boolean SPOOF_STREAM = SPOOF_CLIENT && SPOOF_STRATEGY == SpoofClientStrategy.REPLACE_STREAMS; /** * Any unreachable ip address. Used to intentionally fail requests. @@ -50,7 +44,7 @@ public class SpoofClientPatch { * @return An unreachable URI if the request is a /get_watch request, otherwise the original URI. */ public static Uri blockGetWatchRequest(Uri playerRequestUri) { - if (SPOOF_CLIENT || SPOOF_STREAM) { + if (SPOOF_CLIENT) { try { String path = playerRequestUri.getPath(); @@ -73,7 +67,7 @@ public static Uri blockGetWatchRequest(Uri playerRequestUri) { * Blocks /initplayback requests. */ public static String blockInitPlaybackRequest(String originalUrlString) { - if (SPOOF_CLIENT || SPOOF_STREAM) { + if (SPOOF_CLIENT) { try { var originalUri = Uri.parse(originalUrlString); String path = originalUri.getPath(); @@ -94,51 +88,51 @@ public static String blockInitPlaybackRequest(String originalUrlString) { /** * Injection point. */ - public static int getClientTypeId(int originalClientTypeId) { - return SPOOF_CLIENT ? SPOOF_CLIENT_TYPE.id : originalClientTypeId; + public static boolean isClientTypeSpoofingEnabled() { + return SPOOF_CLIENT_TYPE != null; } /** * Injection point. */ - public static String getClientVersion(String originalClientVersion) { - return SPOOF_CLIENT ? SPOOF_CLIENT_TYPE.appVersion : originalClientVersion; + public static boolean isSpoofStreamEnabled() { + return SPOOF_STREAM; } /** * Injection point. */ - public static String getClientModel(String originalClientModel) { - return SPOOF_CLIENT ? SPOOF_CLIENT_TYPE.model : originalClientModel; + public static int getClientTypeId(int originalClientTypeId) { + return SPOOF_CLIENT_TYPE != null ? SPOOF_CLIENT_TYPE.id : originalClientTypeId; } /** * Injection point. - * Fix video qualities missing, if spoofing to iOS by using the correct client OS version. */ - public static String getOsVersion(String originalOsVersion) { - return SPOOF_CLIENT ? SPOOF_CLIENT_TYPE.osVersion : originalOsVersion; + public static String getClientVersion(String originalClientVersion) { + return SPOOF_CLIENT_TYPE != null ? SPOOF_CLIENT_TYPE.appVersion : originalClientVersion; } /** * Injection point. */ - public static boolean enablePlayerGesture(boolean original) { - return SPOOF_CLIENT || original; + public static String getClientModel(String originalClientModel) { + return SPOOF_CLIENT_TYPE != null ? SPOOF_CLIENT_TYPE.model : originalClientModel; } /** * Injection point. + * Fix video qualities missing, if spoofing to iOS by using the correct client OS version. */ - public static boolean isClientSpoofingEnabled() { - return SPOOF_CLIENT; + public static String getOsVersion(String originalOsVersion) { + return SPOOF_CLIENT_TYPE != null ? SPOOF_CLIENT_TYPE.osVersion : originalOsVersion; } /** * Injection point. */ - public static boolean isSpoofStreamEnabled() { - return SPOOF_STREAM; + public static boolean enablePlayerGesture(boolean original) { + return SPOOF_STRATEGY.enablePlayerGesture || original; } /** @@ -147,7 +141,7 @@ public static boolean isSpoofStreamEnabled() { * Return true to force create the playback speed menu. */ public static boolean forceCreatePlaybackSpeedMenu(boolean original) { - return SPOOF_IOS || original; + return SPOOF_STRATEGY.forceCreatePlaybackSpeedMenu || original; } /** @@ -156,7 +150,7 @@ public static boolean forceCreatePlaybackSpeedMenu(boolean original) { * Return true to force enable audio background play. */ public static boolean overrideBackgroundAudioPlayback() { - return SPOOF_IOS && BackgroundPlaybackPatch.playbackIsNotShort(); + return SPOOF_STRATEGY.overrideBackgroundAudioPlayback && BackgroundPlaybackPatch.playbackIsNotShort(); } /** @@ -164,14 +158,15 @@ public static boolean overrideBackgroundAudioPlayback() { */ public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Builder builder, String url, Map playerHeaders) { - if (SPOOF_CLIENT || SPOOF_STREAM) { + if (SPOOF_CLIENT) { Uri uri = Uri.parse(url); String path = uri.getPath(); if (path != null && path.contains("player") && !path.contains("heartbeat")) { - if (SPOOF_CLIENT) { + if (SPOOF_CLIENT_TYPE != null) { Logger.printDebug(() -> "Overriding user agent for /player call"); builder.addHeader("User-Agent", SPOOF_CLIENT_TYPE.userAgent); } else { + // Spoof stream. String videoId = uri.getQueryParameter("id"); currentVideoStream = StreamingDataRequester.fetch(videoId, playerHeaders); } @@ -240,136 +235,43 @@ public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] pos return postData; } - // Must check for device features in a separate class and cannot place this code inside - // the Patch or ClientType enum due to cyclic Setting references. - static class DeviceHardwareSupport { - private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding(); - private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding(); - - private static boolean deviceHasVP9HardwareDecoding() { - MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); - - for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - ? codecInfo.isHardwareAccelerated() - : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. - if (isHardwareAccelerated && !codecInfo.isEncoder()) { - for (String type : codecInfo.getSupportedTypes()) { - if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { - Logger.printDebug(() -> "Device supports VP9 hardware decoding."); - return true; - } - } - } - } - - Logger.printDebug(() -> "Device does not support VP9 hardware decoding."); - return false; - } - - private static boolean deviceHasAV1HardwareDecoding() { - // It appears all devices with hardware AV1 are also Android 10 or newer. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); - - for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) { - for (String type : codecInfo.getSupportedTypes()) { - if (type.equalsIgnoreCase("video/av01")) { - Logger.printDebug(() -> "Device supports AV1 hardware decoding."); - return true; - } - } - } - } - } - - Logger.printDebug(() -> "Device does not support AV1 hardware decoding."); - return false; - } - - static boolean allowVP9() { - return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_CLIENT_IOS_FORCE_AVC.get(); - } - - static boolean allowAV1() { - return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1; + public enum SpoofClientStrategy { + REPLACE_STREAMS(null, + false, + false, + false), + CLIENT_IOS(ClientType.IOS, + true, + true, + true), + CLIENT_ANDROID_VR(ClientType.ANDROID_VR, + false, + false, + false); + + private final ClientType clientType; + private final boolean forceCreatePlaybackSpeedMenu; + private final boolean overrideBackgroundAudioPlayback; + private final boolean enablePlayerGesture; + + SpoofClientStrategy(ClientType clientType, + boolean forceCreatePlaybackSpeedMenu, + boolean overrideBackgroundAudioPlayback, + boolean enablePlayerGesture) { + this.clientType = clientType; + this.forceCreatePlaybackSpeedMenu = forceCreatePlaybackSpeedMenu; + this.overrideBackgroundAudioPlayback = overrideBackgroundAudioPlayback; + this.enablePlayerGesture = enablePlayerGesture; } } - public enum ClientType { - // https://dumps.tadiphone.dev/dumps/oculus/eureka - IOS(5, - // iPhone 15 supports AV1 hardware decoding. - // Only use if this Android device also has hardware decoding. - allowAV1() - ? "iPhone16,2" // 15 Pro Max - : "iPhone11,4", // XS Max - // iOS 14+ forces VP9. - allowVP9() - ? "17.5.1.21F90" - : "13.7.17H35", - allowVP9() - ? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)" - : "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)", - // Version number should be a valid iOS release. - // https://www.ipa4fun.com/history/185230 - "19.10.7" - ), - ANDROID_VR(28, - "Quest 3", - "12", - "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", - "1.56.21" - ), - ANDROID(3, - Build.MODEL, - Build.VERSION.RELEASE, - String.format("com.google.android.youtube/%s (Linux; U; Android %s; GB) gzip", - Utils.getAppVersionName(), Build.VERSION.RELEASE), - Utils.getAppVersionName() - ); - - /** - * YouTube - * client type - */ - public final int id; - - /** - * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) - */ - public final String model; - - /** - * Device OS version. - */ - public final String osVersion; - - /** - * Player user-agent. - */ - public final String userAgent; - - /** - * App version. - */ - public final String appVersion; - - ClientType(int id, String model, String osVersion, String userAgent, String appVersion) { - this.id = id; - this.model = model; - this.osVersion = osVersion; - this.userAgent = userAgent; - this.appVersion = appVersion; - } - } - - public static final class ForceiOSAVCAvailability implements Setting.Availability { + public static final class ForceAVCAvailability implements Setting.Availability { @Override public boolean isAvailable() { - return (Settings.SPOOF_CLIENT.get() && Settings.SPOOF_CLIENT_TYPE.get() == ClientType.IOS) - || Settings.SPOOF_STREAM.get(); + if (!Settings.SPOOF_CLIENT.get()) return false; + + SpoofClientStrategy strategy = Settings.SPOOF_CLIENT_STRATEGY.get(); + return strategy == SpoofClientStrategy.CLIENT_IOS || strategy == SpoofClientStrategy.REPLACE_STREAMS; } } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java index fd0977d8a5..b3c590d6ef 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java @@ -1,6 +1,6 @@ package app.revanced.integrations.youtube.patches.spoof.requests; -import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; +import app.revanced.integrations.youtube.patches.spoof.ClientType; import app.revanced.integrations.youtube.requests.Requester; import app.revanced.integrations.youtube.requests.Route; import app.revanced.integrations.shared.Logger; diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java index 200d5fb0d1..b43f040f35 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java @@ -3,8 +3,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import app.revanced.integrations.shared.settings.BaseSettings; +import app.revanced.integrations.youtube.patches.spoof.ClientType; import app.revanced.integrations.youtube.patches.spoof.StoryboardRenderer; -import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; import app.revanced.integrations.youtube.requests.Requester; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java index e00b333006..e5e102a900 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java @@ -20,7 +20,7 @@ import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; import app.revanced.integrations.shared.settings.BaseSettings; -import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; +import app.revanced.integrations.youtube.patches.spoof.ClientType; public class StreamingDataRequester { diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 077811523c..d99517f306 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -6,7 +6,7 @@ import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3; -import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; +import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.SpoofClientStrategy; import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*; import java.util.Arrays; @@ -256,9 +256,9 @@ public class Settings extends BaseSettings { public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE); public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE); public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true,"revanced_spoof_client_user_dialog_message"); - public static final BooleanSetting SPOOF_CLIENT_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_client_ios_force_avc", FALSE, true, - "revanced_spoof_client_ios_force_avc_user_dialog_message", new SpoofClientPatch.ForceiOSAVCAvailability()); - public static final EnumSetting SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", ClientType.IOS, true, parent(SPOOF_CLIENT)); + public static final BooleanSetting SPOOF_CLIENT_FORCE_AVC = new BooleanSetting("revanced_spoof_client_force_avc", FALSE, true, + "revanced_spoof_client_force_avc_user_dialog_message", new SpoofClientPatch.ForceAVCAvailability()); + public static final EnumSetting SPOOF_CLIENT_STRATEGY = new EnumSetting<>("revanced_spoof_client_strategy", SpoofClientStrategy.REPLACE_STREAMS, true, parent(SPOOF_CLIENT)); public static final BooleanSetting SPOOF_STREAM = new BooleanSetting("revanced_spoof_stream", FALSE, true); @Deprecated public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", ""); From 6649e0cdb9778622a9ea4027011a8e227fe6475a Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 26 Aug 2024 03:31:46 -0400 Subject: [PATCH 07/91] refactor --- .../youtube/patches/spoof/ClientType.java | 17 +++++++++++++++-- .../youtube/patches/spoof/SpoofClientPatch.java | 4 ++-- .../patches/spoof/requests/PlayerRoutes.java | 4 ++-- .../spoof/requests/StreamingDataRequester.java | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java index e99d5dc6b8..c63addea2e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java @@ -5,6 +5,8 @@ import android.os.Build; +import androidx.annotation.Nullable; + import app.revanced.integrations.shared.Utils; public enum ClientType { @@ -22,6 +24,7 @@ public enum ClientType { allowVP9() ? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)" : "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)", + null, // Version number should be a valid iOS release. // https://www.ipa4fun.com/history/185230 "19.10.7" @@ -30,7 +33,8 @@ public enum ClientType { "Quest 3", "12", "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", - "1.56.21" + "1.56.21", + "34" ), @Deprecated() // Android spoofing in this context no longer works. ANDROID(3, @@ -38,6 +42,7 @@ public enum ClientType { Build.VERSION.RELEASE, String.format("com.google.android.youtube/%s (Linux; U; Android %s; GB) gzip", Utils.getAppVersionName(), Build.VERSION.RELEASE), + Build.VERSION.SDK, Utils.getAppVersionName() ); @@ -67,11 +72,19 @@ public enum ClientType { */ public final String appVersion; - ClientType(int id, String model, String osVersion, String userAgent, String appVersion) { + /** + * Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk) + * Field is null if not applicable. + */ + @Nullable + public final String androidSdkVersion; + + ClientType(int id, String model, String osVersion, String userAgent, String androidSdkVersion, String appVersion) { this.id = id; this.model = model; this.osVersion = osVersion; this.userAgent = userAgent; + this.androidSdkVersion = androidSdkVersion; this.appVersion = appVersion; } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 8a3603d3e9..dc17f6e9fc 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -189,8 +189,8 @@ public static ByteBuffer getStreamingData(String videoId) { var future = currentVideoStream; if (future != null) { - final long maxTimeToWait = 4000; - var stream = future.get(maxTimeToWait, TimeUnit.MILLISECONDS); + final long maxSecondsToWait = 20; + var stream = future.get(maxSecondsToWait, TimeUnit.SECONDS); if (stream != null) { Logger.printDebug(() -> "Overriding video stream"); return stream; diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java index b3c590d6ef..7444602c8b 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java @@ -47,8 +47,8 @@ static String createInnertubeBody(ClientType clientType) { client.put("clientVersion", clientType.appVersion); client.put("deviceModel", clientType.model); client.put("osVersion", clientType.osVersion); - if (clientType != ClientType.IOS) { - client.put("androidSdkVersion", 34); + if (clientType.androidSdkVersion != null) { + client.put("androidSdkVersion", clientType.androidSdkVersion); } context.put("client", client); diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java index e5e102a900..76e44767c8 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java @@ -93,7 +93,7 @@ public static Future fetch(@NonNull String videoId, Map= 0) { baos.write(buffer, 0, bytesRead); } return ByteBuffer.wrap(baos.toByteArray()); From 3c31b6a0a712b747fed3a38eb339d20da6d9de3a Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 26 Aug 2024 03:44:25 -0400 Subject: [PATCH 08/91] fix: Close byte array stream, do not show toast if all streams return 0 byte content --- .../spoof/requests/StreamingDataRequester.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java index 76e44767c8..1c4521d223 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java @@ -90,13 +90,17 @@ public static Future fetch(@NonNull String videoId, Map= 0) { - baos.write(buffer, 0, bytesRead); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) >= 0) { + baos.write(buffer, 0, bytesRead); + } + + byte[] array = baos.toByteArray(); + Logger.printDebug(() -> "stream response: " + new String(array, StandardCharsets.UTF_8)); // FIXME debug + return ByteBuffer.wrap(array); } - return ByteBuffer.wrap(baos.toByteArray()); } } } catch (IOException ex) { @@ -105,7 +109,7 @@ public static Future fetch(@NonNull String videoId, Map Date: Mon, 26 Aug 2024 03:45:08 -0400 Subject: [PATCH 09/91] fix: Remove debug code --- .../patches/spoof/requests/StreamingDataRequester.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java index 1c4521d223..1ef02a34a5 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java @@ -97,9 +97,7 @@ public static Future fetch(@NonNull String videoId, Map "stream response: " + new String(array, StandardCharsets.UTF_8)); // FIXME debug - return ByteBuffer.wrap(array); + return ByteBuffer.wrap(baos.toByteArray()); } } } From 36e072c0b2addd9732a312d8e573c6db05d68e4c Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 26 Aug 2024 03:57:08 -0400 Subject: [PATCH 10/91] fix: Show error toast only if last attempt fails. Add more logging. --- .../requests/StreamingDataRequester.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java index 1ef02a34a5..2aabe51e27 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java @@ -33,12 +33,11 @@ private static void handleConnectionError(String toastMessage, @Nullable Excepti } @Nullable - private static HttpURLConnection send(ClientType clientType, String videoId, Map playerHeaders) { + private static HttpURLConnection send(ClientType clientType, String videoId, + Map playerHeaders, + boolean showErrorToasts) { final long startTime = System.currentTimeMillis(); String clientTypeName = clientType.name(); - // Only show toast for each attempt if debug mode is enabled, - // as the calling code shows a toast if all stream calls fail. - final boolean showErrorToasts = BaseSettings.DEBUG.get(); Logger.printDebug(() -> "Fetching video streams using client: " + clientType.name()); try { @@ -57,7 +56,8 @@ private static HttpURLConnection send(ClientType clientType, String videoId, Map final int responseCode = connection.getResponseCode(); if (responseCode == 200) return connection; - handleConnectionError(clientTypeName + " not available with response code: " + responseCode, + handleConnectionError(clientTypeName + " not available with response code: " + + responseCode + " message: " + connection.getResponseMessage(), null, showErrorToasts); } catch (SocketTimeoutException ex) { handleConnectionError("Connection timeout", ex, showErrorToasts); @@ -82,8 +82,13 @@ public static Future fetch(@NonNull String videoId, Map fetch(@NonNull String videoId, Map Date: Mon, 26 Aug 2024 04:27:47 -0400 Subject: [PATCH 11/91] fix refactoring typo, use correct Android SDK version --- .../integrations/youtube/patches/spoof/ClientType.java | 4 ++-- .../patches/spoof/requests/StreamingDataRequester.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java index c63addea2e..31af89b016 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java @@ -33,8 +33,8 @@ public enum ClientType { "Quest 3", "12", "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", - "1.56.21", - "34" + "32", // Android 12 SDK is version 31, but Quest 3 build.props uses version 32. + "1.56.21" ), @Deprecated() // Android spoofing in this context no longer works. ANDROID(3, diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java index 2aabe51e27..f96ff21fa8 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java @@ -86,7 +86,7 @@ public static Future fetch(@NonNull String videoId, Map Date: Mon, 26 Aug 2024 04:44:56 -0400 Subject: [PATCH 12/91] fix: If debug is enabled, then use a random client order --- .../spoof/requests/StreamingDataRequester.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java index f96ff21fa8..8f809a1106 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java @@ -13,8 +13,7 @@ import java.net.SocketTimeoutException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.concurrent.Future; import app.revanced.integrations.shared.Logger; @@ -82,8 +81,14 @@ public static Future fetch(@NonNull String videoId, Map Date: Mon, 26 Aug 2024 04:51:46 -0400 Subject: [PATCH 13/91] fix: Comments, warnings --- .../revanced/integrations/youtube/patches/spoof/ClientType.java | 2 +- .../youtube/patches/spoof/requests/StreamingDataRequester.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java index 31af89b016..42a7650908 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java @@ -79,7 +79,7 @@ public enum ClientType { @Nullable public final String androidSdkVersion; - ClientType(int id, String model, String osVersion, String userAgent, String androidSdkVersion, String appVersion) { + ClientType(int id, String model, String osVersion, String userAgent, @Nullable String androidSdkVersion, String appVersion) { this.id = id; this.model = model; this.osVersion = osVersion; diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java index 8f809a1106..113fd264f8 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java @@ -90,7 +90,7 @@ public static Future fetch(@NonNull String videoId, Map Date: Mon, 26 Aug 2024 13:58:05 -0400 Subject: [PATCH 14/91] fix: Don't override player responses when all spoofing is off --- .../youtube/patches/spoof/SpoofClientPatch.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index dc17f6e9fc..da78aa1d40 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -132,7 +132,7 @@ public static String getOsVersion(String originalOsVersion) { * Injection point. */ public static boolean enablePlayerGesture(boolean original) { - return SPOOF_STRATEGY.enablePlayerGesture || original; + return SPOOF_CLIENT && SPOOF_STRATEGY.enablePlayerGesture || original; } /** @@ -141,7 +141,7 @@ public static boolean enablePlayerGesture(boolean original) { * Return true to force create the playback speed menu. */ public static boolean forceCreatePlaybackSpeedMenu(boolean original) { - return SPOOF_STRATEGY.forceCreatePlaybackSpeedMenu || original; + return SPOOF_CLIENT && SPOOF_STRATEGY.forceCreatePlaybackSpeedMenu || original; } /** @@ -150,7 +150,8 @@ public static boolean forceCreatePlaybackSpeedMenu(boolean original) { * Return true to force enable audio background play. */ public static boolean overrideBackgroundAudioPlayback() { - return SPOOF_STRATEGY.overrideBackgroundAudioPlayback && BackgroundPlaybackPatch.playbackIsNotShort(); + return SPOOF_CLIENT && SPOOF_STRATEGY.overrideBackgroundAudioPlayback + && BackgroundPlaybackPatch.playbackIsNotShort(); } /** From 9b1e84b28f1bb7831ab83b316ffc5f44a97f53f8 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:02:32 -0400 Subject: [PATCH 15/91] fix: Remove unused setting --- .../app/revanced/integrations/youtube/settings/Settings.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index d99517f306..673cabeebd 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -259,7 +259,6 @@ public class Settings extends BaseSettings { public static final BooleanSetting SPOOF_CLIENT_FORCE_AVC = new BooleanSetting("revanced_spoof_client_force_avc", FALSE, true, "revanced_spoof_client_force_avc_user_dialog_message", new SpoofClientPatch.ForceAVCAvailability()); public static final EnumSetting SPOOF_CLIENT_STRATEGY = new EnumSetting<>("revanced_spoof_client_strategy", SpoofClientStrategy.REPLACE_STREAMS, true, parent(SPOOF_CLIENT)); - public static final BooleanSetting SPOOF_STREAM = new BooleanSetting("revanced_spoof_stream", FALSE, true); @Deprecated public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", ""); public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1); From bfe7faa74f66e66448c2baecce694db4a28ae1d2 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:28:35 -0400 Subject: [PATCH 16/91] fix: Remove client type spoofing and use only stream replacement --- .../youtube/patches/spoof/ClientType.java | 10 +- .../patches/spoof/SpoofClientPatch.java | 126 +----------------- .../patches/spoof/SpoofSignaturePatch.java | 2 +- .../patches/spoof/requests/PlayerRoutes.java | 10 +- .../youtube/settings/Settings.java | 5 +- 5 files changed, 19 insertions(+), 134 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java index 42a7650908..d73e17f4d3 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java @@ -67,11 +67,6 @@ public enum ClientType { */ public final String userAgent; - /** - * App version. - */ - public final String appVersion; - /** * Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk) * Field is null if not applicable. @@ -79,6 +74,11 @@ public enum ClientType { @Nullable public final String androidSdkVersion; + /** + * App version. + */ + public final String appVersion; + ClientType(int id, String model, String osVersion, String userAgent, @Nullable String androidSdkVersion, String appVersion) { this.id = id; this.model = model; diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index da78aa1d40..5a5347cf45 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -15,18 +15,12 @@ import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; -import app.revanced.integrations.shared.settings.Setting; -import app.revanced.integrations.youtube.patches.BackgroundPlaybackPatch; import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequester; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") public class SpoofClientPatch { private static final boolean SPOOF_CLIENT = Settings.SPOOF_CLIENT.get(); - private static final SpoofClientStrategy SPOOF_STRATEGY = Settings.SPOOF_CLIENT_STRATEGY.get(); - @Nullable - private static final ClientType SPOOF_CLIENT_TYPE = SPOOF_CLIENT ? SPOOF_STRATEGY.clientType : null; - private static final boolean SPOOF_STREAM = SPOOF_CLIENT && SPOOF_STRATEGY == SpoofClientStrategy.REPLACE_STREAMS; /** * Any unreachable ip address. Used to intentionally fail requests. @@ -88,70 +82,8 @@ public static String blockInitPlaybackRequest(String originalUrlString) { /** * Injection point. */ - public static boolean isClientTypeSpoofingEnabled() { - return SPOOF_CLIENT_TYPE != null; - } - - /** - * Injection point. - */ - public static boolean isSpoofStreamEnabled() { - return SPOOF_STREAM; - } - - /** - * Injection point. - */ - public static int getClientTypeId(int originalClientTypeId) { - return SPOOF_CLIENT_TYPE != null ? SPOOF_CLIENT_TYPE.id : originalClientTypeId; - } - - /** - * Injection point. - */ - public static String getClientVersion(String originalClientVersion) { - return SPOOF_CLIENT_TYPE != null ? SPOOF_CLIENT_TYPE.appVersion : originalClientVersion; - } - - /** - * Injection point. - */ - public static String getClientModel(String originalClientModel) { - return SPOOF_CLIENT_TYPE != null ? SPOOF_CLIENT_TYPE.model : originalClientModel; - } - - /** - * Injection point. - * Fix video qualities missing, if spoofing to iOS by using the correct client OS version. - */ - public static String getOsVersion(String originalOsVersion) { - return SPOOF_CLIENT_TYPE != null ? SPOOF_CLIENT_TYPE.osVersion : originalOsVersion; - } - - /** - * Injection point. - */ - public static boolean enablePlayerGesture(boolean original) { - return SPOOF_CLIENT && SPOOF_STRATEGY.enablePlayerGesture || original; - } - - /** - * Injection point. - * When spoofing the client to iOS, the playback speed menu is missing from the player response. - * Return true to force create the playback speed menu. - */ - public static boolean forceCreatePlaybackSpeedMenu(boolean original) { - return SPOOF_CLIENT && SPOOF_STRATEGY.forceCreatePlaybackSpeedMenu || original; - } - - /** - * Injection point. - * When spoofing the client to iOS, background audio only playback of livestreams fails. - * Return true to force enable audio background play. - */ - public static boolean overrideBackgroundAudioPlayback() { - return SPOOF_CLIENT && SPOOF_STRATEGY.overrideBackgroundAudioPlayback - && BackgroundPlaybackPatch.playbackIsNotShort(); + public static boolean isSpoofingEnabled() { + return SPOOF_CLIENT; } /** @@ -163,14 +95,8 @@ public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Bu Uri uri = Uri.parse(url); String path = uri.getPath(); if (path != null && path.contains("player") && !path.contains("heartbeat")) { - if (SPOOF_CLIENT_TYPE != null) { - Logger.printDebug(() -> "Overriding user agent for /player call"); - builder.addHeader("User-Agent", SPOOF_CLIENT_TYPE.userAgent); - } else { - // Spoof stream. - String videoId = uri.getQueryParameter("id"); - currentVideoStream = StreamingDataRequester.fetch(videoId, playerHeaders); - } + String videoId = uri.getQueryParameter("id"); + currentVideoStream = StreamingDataRequester.fetch(videoId, playerHeaders); } } @@ -184,7 +110,7 @@ public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Bu */ @Nullable public static ByteBuffer getStreamingData(String videoId) { - if (SPOOF_STREAM) { + if (SPOOF_CLIENT) { try { Utils.verifyOffMainThread(); @@ -217,7 +143,7 @@ public static ByteBuffer getStreamingData(String videoId) { * Called after {@link #getStreamingData(String)}. */ public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) { - if (SPOOF_STREAM) { + if (SPOOF_CLIENT) { try { final int methodPost = 2; if (method == methodPost) { @@ -235,44 +161,4 @@ public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] pos return postData; } - - public enum SpoofClientStrategy { - REPLACE_STREAMS(null, - false, - false, - false), - CLIENT_IOS(ClientType.IOS, - true, - true, - true), - CLIENT_ANDROID_VR(ClientType.ANDROID_VR, - false, - false, - false); - - private final ClientType clientType; - private final boolean forceCreatePlaybackSpeedMenu; - private final boolean overrideBackgroundAudioPlayback; - private final boolean enablePlayerGesture; - - SpoofClientStrategy(ClientType clientType, - boolean forceCreatePlaybackSpeedMenu, - boolean overrideBackgroundAudioPlayback, - boolean enablePlayerGesture) { - this.clientType = clientType; - this.forceCreatePlaybackSpeedMenu = forceCreatePlaybackSpeedMenu; - this.overrideBackgroundAudioPlayback = overrideBackgroundAudioPlayback; - this.enablePlayerGesture = enablePlayerGesture; - } - } - - public static final class ForceAVCAvailability implements Setting.Availability { - @Override - public boolean isAvailable() { - if (!Settings.SPOOF_CLIENT.get()) return false; - - SpoofClientStrategy strategy = Settings.SPOOF_CLIENT_STRATEGY.get(); - return strategy == SpoofClientStrategy.CLIENT_IOS || strategy == SpoofClientStrategy.REPLACE_STREAMS; - } - } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java index 41f03ed781..65579cb12d 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java @@ -19,7 +19,7 @@ import static app.revanced.integrations.youtube.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer; /** @noinspection unused*/ -@Deprecated +@Deprecated // Patch is obsolete and no longer works. public class SpoofSignaturePatch { /** * Parameter (also used by diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java index 7444602c8b..5a2be5fd52 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java @@ -1,18 +1,20 @@ package app.revanced.integrations.youtube.patches.spoof.requests; -import app.revanced.integrations.youtube.patches.spoof.ClientType; -import app.revanced.integrations.youtube.requests.Requester; -import app.revanced.integrations.youtube.requests.Route; -import app.revanced.integrations.shared.Logger; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.net.HttpURLConnection; +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.youtube.patches.spoof.ClientType; +import app.revanced.integrations.youtube.requests.Requester; +import app.revanced.integrations.youtube.requests.Route; + final class PlayerRoutes { private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"; + @Deprecated static final Route.CompiledRoute GET_STORYBOARD_SPEC_RENDERER = new Route( Route.Method.POST, "player" + diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 673cabeebd..5a71ea79c8 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -6,7 +6,6 @@ import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3; -import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.SpoofClientStrategy; import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*; import java.util.Arrays; @@ -21,7 +20,6 @@ import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.ThumbnailOption; import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.ThumbnailStillTime; import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch; -import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch; import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings; @SuppressWarnings("deprecation") @@ -257,8 +255,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE); public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true,"revanced_spoof_client_user_dialog_message"); public static final BooleanSetting SPOOF_CLIENT_FORCE_AVC = new BooleanSetting("revanced_spoof_client_force_avc", FALSE, true, - "revanced_spoof_client_force_avc_user_dialog_message", new SpoofClientPatch.ForceAVCAvailability()); - public static final EnumSetting SPOOF_CLIENT_STRATEGY = new EnumSetting<>("revanced_spoof_client_strategy", SpoofClientStrategy.REPLACE_STREAMS, true, parent(SPOOF_CLIENT)); + "revanced_spoof_client_force_avc_user_dialog_message", parent(SPOOF_CLIENT)); @Deprecated public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", ""); public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1); From 69a1a880b73a1a46be5797397f45d296b9f05c59 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:44:36 -0400 Subject: [PATCH 17/91] Fix: Remove obsolete code that no longer works --- .../youtube/patches/spoof/ClientType.java | 11 - .../patches/spoof/SpoofSignaturePatch.java | 242 ------------------ .../patches/spoof/StoryboardRenderer.java | 36 --- .../patches/spoof/requests/PlayerRoutes.java | 9 - .../requests/StoryboardRendererRequester.java | 162 ------------ 5 files changed, 460 deletions(-) delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java index d73e17f4d3..a906f0739f 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java @@ -7,8 +7,6 @@ import androidx.annotation.Nullable; -import app.revanced.integrations.shared.Utils; - public enum ClientType { // https://dumps.tadiphone.dev/dumps/oculus/eureka IOS(5, @@ -35,15 +33,6 @@ public enum ClientType { "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", "32", // Android 12 SDK is version 31, but Quest 3 build.props uses version 32. "1.56.21" - ), - @Deprecated() // Android spoofing in this context no longer works. - ANDROID(3, - Build.MODEL, - Build.VERSION.RELEASE, - String.format("com.google.android.youtube/%s (Linux; U; Android %s; GB) gzip", - Utils.getAppVersionName(), Build.VERSION.RELEASE), - Build.VERSION.SDK, - Utils.getAppVersionName() ); /** diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java deleted file mode 100644 index 65579cb12d..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java +++ /dev/null @@ -1,242 +0,0 @@ -package app.revanced.integrations.youtube.patches.spoof; - -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import androidx.annotation.Nullable; -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; -import app.revanced.integrations.youtube.patches.VideoInformation; -import app.revanced.integrations.youtube.settings.Settings; -import app.revanced.integrations.youtube.shared.PlayerType; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import static app.revanced.integrations.shared.Utils.containsAny; -import static app.revanced.integrations.youtube.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer; - -/** @noinspection unused*/ -@Deprecated // Patch is obsolete and no longer works. -public class SpoofSignaturePatch { - /** - * Parameter (also used by - * yt-dlp) - * to fix playback issues. - */ - private static final String INCOGNITO_PARAMETERS = "CgIQBg=="; - - /** - * Parameters used when playing clips. - */ - private static final String CLIPS_PARAMETERS = "kAIB"; - - /** - * Parameters causing playback issues. - */ - private static final String[] AUTOPLAY_PARAMETERS = { - "YAHI", // Autoplay in feed. - "SAFg" // Autoplay in scrim. - }; - - /** - * Parameter used for autoplay in scrim. - * Prepend this parameter to mute video playback (for autoplay in feed). - */ - private static final String SCRIM_PARAMETER = "SAFgAXgB"; - - /** - * Last video id loaded. Used to prevent reloading the same spec multiple times. - */ - @Nullable - private static volatile String lastPlayerResponseVideoId; - - @Nullable - private static volatile Future rendererFuture; - - private static volatile boolean useOriginalStoryboardRenderer; - - private static volatile boolean isPlayingShorts; - - @Nullable - private static StoryboardRenderer getRenderer(boolean waitForCompletion) { - Future future = rendererFuture; - if (future != null) { - try { - if (waitForCompletion || future.isDone()) { - return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout. - } // else, return null. - } catch (TimeoutException ex) { - Logger.printDebug(() -> "Could not get renderer (get timed out)"); - } catch (ExecutionException | InterruptedException ex) { - // Should never happen. - Logger.printException(() -> "Could not get renderer", ex); - } - } - return null; - } - - /** - * Injection point. - * - * Called off the main thread, and called multiple times for each video. - * - * @param parameters Original protobuf parameter value. - */ - public static String spoofParameter(String parameters, String videoId, boolean isShortAndOpeningOrPlaying) { - try { - Logger.printDebug(() -> "Original protobuf parameter value: " + parameters); - - if (parameters == null || !Settings.SPOOF_SIGNATURE.get()) { - return parameters; - } - - // Clip's player parameters contain a lot of information (e.g. video start and end time or whether it loops) - // For this reason, the player parameters of a clip are usually very long (150~300 characters). - // Clips are 60 seconds or less in length, so no spoofing. - //noinspection AssignmentUsedAsCondition - if (useOriginalStoryboardRenderer = parameters.length() > 150 || parameters.startsWith(CLIPS_PARAMETERS)) { - return parameters; - } - - // Shorts do not need to be spoofed. - //noinspection AssignmentUsedAsCondition - if (useOriginalStoryboardRenderer = VideoInformation.playerParametersAreShort(parameters)) { - isPlayingShorts = true; - return parameters; - } - isPlayingShorts = false; - - boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL - && containsAny(parameters, AUTOPLAY_PARAMETERS); - if (isPlayingFeed) { - //noinspection AssignmentUsedAsCondition - if (useOriginalStoryboardRenderer = !Settings.SPOOF_SIGNATURE_IN_FEED.get()) { - // Don't spoof the feed video playback. This will cause video playback issues, - // but only if user continues watching for more than 1 minute. - return parameters; - } - // Spoof the feed video. Video will show up in watch history and video subtitles are missing. - fetchStoryboardRenderer(); - return SCRIM_PARAMETER + INCOGNITO_PARAMETERS; - } - - fetchStoryboardRenderer(); - } catch (Exception ex) { - Logger.printException(() -> "spoofParameter failure", ex); - } - return INCOGNITO_PARAMETERS; - } - - private static void fetchStoryboardRenderer() { - if (!Settings.SPOOF_STORYBOARD_RENDERER.get()) { - lastPlayerResponseVideoId = null; - rendererFuture = null; - return; - } - String videoId = VideoInformation.getPlayerResponseVideoId(); - if (!videoId.equals(lastPlayerResponseVideoId)) { - rendererFuture = Utils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId)); - lastPlayerResponseVideoId = videoId; - } - // Block until the renderer fetch completes. - // This is desired because if this returns without finishing the fetch - // then video will start playback but the storyboard is not ready yet. - getRenderer(true); - } - - private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec, - boolean returnNullIfLiveStream) { - if (Settings.SPOOF_SIGNATURE.get() && !useOriginalStoryboardRenderer) { - StoryboardRenderer renderer = getRenderer(false); - if (renderer != null) { - if (returnNullIfLiveStream && renderer.isLiveStream) { - return null; - } - - if (renderer.spec != null) { - return renderer.spec; - } - } - } - - return originalStoryboardRendererSpec; - } - - /** - * Injection point. - * Called from background threads and from the main thread. - */ - @Nullable - public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) { - return getStoryboardRendererSpec(originalStoryboardRendererSpec, false); - } - - /** - * Injection point. - * Uses additional check to handle live streams. - * Called from background threads and from the main thread. - */ - @Nullable - public static String getStoryboardDecoderRendererSpec(String originalStoryboardRendererSpec) { - return getStoryboardRendererSpec(originalStoryboardRendererSpec, true); - } - - /** - * Injection point. - */ - public static int getRecommendedLevel(int originalLevel) { - if (Settings.SPOOF_SIGNATURE.get() && !useOriginalStoryboardRenderer) { - StoryboardRenderer renderer = getRenderer(false); - if (renderer != null) { - if (renderer.recommendedLevel != null) { - return renderer.recommendedLevel; - } - } - } - - return originalLevel; - } - - /** - * Injection point. Forces seekbar to be shown for paid videos or - * if {@link Settings#SPOOF_STORYBOARD_RENDERER} is not enabled. - */ - public static boolean getSeekbarThumbnailOverrideValue() { - if (!Settings.SPOOF_SIGNATURE.get()) { - return false; - } - StoryboardRenderer renderer = getRenderer(false); - if (renderer == null) { - // Spoof storyboard renderer is turned off, - // video is paid, or the storyboard fetch timed out. - // Show empty thumbnails so the seek time and chapters still show up. - return true; - } - return renderer.spec != null; - } - - /** - * Injection point. - * - * @param view seekbar thumbnail view. Includes both shorts and regular videos. - */ - public static void seekbarImageViewCreated(ImageView view) { - try { - if (!Settings.SPOOF_SIGNATURE.get() - || Settings.SPOOF_STORYBOARD_RENDERER.get()) { - return; - } - if (isPlayingShorts) return; - - view.setVisibility(View.GONE); - // Also hide the border around the thumbnail (otherwise a 1 pixel wide bordered frame is visible). - ViewGroup parentLayout = (ViewGroup) view.getParent(); - parentLayout.setPadding(0, 0, 0, 0); - } catch (Exception ex) { - Logger.printException(() -> "seekbarImageViewCreated failure", ex); - } - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java deleted file mode 100644 index 5014a5fcdc..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java +++ /dev/null @@ -1,36 +0,0 @@ -package app.revanced.integrations.youtube.patches.spoof; - -import androidx.annotation.Nullable; - -import org.jetbrains.annotations.NotNull; - -@Deprecated -public final class StoryboardRenderer { - public final String videoId; - @Nullable - public final String spec; - public final boolean isLiveStream; - /** - * Recommended image quality level, or NULL if no recommendation exists. - */ - @Nullable - public final Integer recommendedLevel; - - public StoryboardRenderer(String videoId, @Nullable String spec, boolean isLiveStream, @Nullable Integer recommendedLevel) { - this.videoId = videoId; - this.spec = spec; - this.isLiveStream = isLiveStream; - this.recommendedLevel = recommendedLevel; - } - - @NotNull - @Override - public String toString() { - return "StoryboardRenderer{" + - "videoId=" + videoId + - ", isLiveStream=" + isLiveStream + - ", spec='" + spec + '\'' + - ", recommendedLevel=" + recommendedLevel + - '}'; - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java index 5a2be5fd52..299110f461 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java @@ -14,15 +14,6 @@ final class PlayerRoutes { private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"; - @Deprecated - static final Route.CompiledRoute GET_STORYBOARD_SPEC_RENDERER = new Route( - Route.Method.POST, - "player" + - "?fields=storyboards.playerStoryboardSpecRenderer," + - "storyboards.playerLiveStoryboardSpecRenderer," + - "playabilityStatus.status" - ).compile(); - static final Route.CompiledRoute GET_STREAMING_DATA = new Route( Route.Method.POST, "player" + diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java deleted file mode 100644 index b43f040f35..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java +++ /dev/null @@ -1,162 +0,0 @@ -package app.revanced.integrations.youtube.patches.spoof.requests; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import app.revanced.integrations.shared.settings.BaseSettings; -import app.revanced.integrations.youtube.patches.spoof.ClientType; -import app.revanced.integrations.youtube.patches.spoof.StoryboardRenderer; -import app.revanced.integrations.youtube.requests.Requester; -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.nio.charset.StandardCharsets; -import java.util.Objects; -import java.util.List; - -import static app.revanced.integrations.shared.StringRef.str; -import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.*; - -@Deprecated -public class StoryboardRendererRequester { - - private StoryboardRendererRequester() { - } - - private static void randomlyWaitIfLocallyDebugging() { - final boolean randomlyWait = false; // Enable to simulate slow connection responses. - if (randomlyWait) { - final long maximumTimeToRandomlyWait = 10000; - Utils.doNothingForDuration(maximumTimeToRandomlyWait); - } - } - - private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex, - boolean showToastOnIOException) { - if (showToastOnIOException) Utils.showToastShort(toastMessage); - Logger.printInfo(() -> toastMessage, ex); - } - - @Nullable - private static JSONObject fetchPlayerResponse(@NonNull String requestBody, ClientType clientType, boolean showToastOnIOException) { - final long startTime = System.currentTimeMillis(); - try { - Utils.verifyOffMainThread(); - Objects.requireNonNull(requestBody); - - final byte[] innerTubeBody = requestBody.getBytes(StandardCharsets.UTF_8); - - HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STORYBOARD_SPEC_RENDERER, clientType); - connection.setRequestProperty("User-Agent", ClientType.ANDROID.userAgent); - connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length); - - final int responseCode = connection.getResponseCode(); - randomlyWaitIfLocallyDebugging(); - if (responseCode == 200) return Requester.parseJSONObject(connection); - - // Always show a toast for this, as a non 200 response means something is broken. - // Not a normal code path and should not be reached, so no translations are needed. - handleConnectionError("Spoof storyboard not available: " + responseCode, - null, showToastOnIOException || BaseSettings.DEBUG_TOAST_ON_ERROR.get()); - connection.disconnect(); - } catch (SocketTimeoutException ex) { - handleConnectionError(str("revanced_spoof_client_storyboard_timeout"), ex, showToastOnIOException); - } catch (IOException ex) { - handleConnectionError(str("revanced_spoof_client_storyboard_io_exception", ex.getMessage()), - ex, showToastOnIOException); - } catch (Exception ex) { - Logger.printException(() -> "Spoof storyboard fetch failed", ex); // Should never happen. - } finally { - Logger.printDebug(() -> "Request took: " + (System.currentTimeMillis() - startTime) + "ms"); - } - - return null; - } - - private static boolean isPlayabilityStatusOk(@NonNull JSONObject playerResponse) { - try { - return playerResponse.getJSONObject("playabilityStatus").getString("status").equals("OK"); - } catch (JSONException e) { - Logger.printDebug(() -> "Failed to get playabilityStatus for response: " + playerResponse); - } - - return false; - } - - /** - * Fetches the storyboardRenderer from the innerTubeBody. - * @return StoryboardRenderer or null if playabilityStatus is not OK. - */ - @Nullable - private static StoryboardRenderer getStoryboardRendererUsingBody(String videoId, - ClientType clientType, - boolean showToastOnIOException) { - String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); - final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody, clientType, showToastOnIOException); - if (playerResponse != null && isPlayabilityStatusOk(playerResponse)) - return getStoryboardRendererUsingResponse(videoId, playerResponse); - - return null; - } - - @Nullable - private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull String videoId, @NonNull JSONObject playerResponse) { - try { - Logger.printDebug(() -> "Parsing response: " + playerResponse); - if (!playerResponse.has("storyboards")) { - Logger.printDebug(() -> "Using empty storyboard"); - return new StoryboardRenderer(videoId, null, false, null); - } - final JSONObject storyboards = playerResponse.getJSONObject("storyboards"); - final boolean isLiveStream = storyboards.has("playerLiveStoryboardSpecRenderer"); - final String storyboardsRendererTag = isLiveStream - ? "playerLiveStoryboardSpecRenderer" - : "playerStoryboardSpecRenderer"; - - final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag); - StoryboardRenderer renderer = new StoryboardRenderer( - videoId, - rendererElement.getString("spec"), - isLiveStream, - rendererElement.has("recommendedLevel") - ? rendererElement.getInt("recommendedLevel") - : null - ); - - Logger.printDebug(() -> "Fetched: " + renderer); - - return renderer; - } catch (JSONException e) { - Logger.printException(() -> "Failed to get storyboardRenderer", e); - } - - return null; - } - - @Nullable - public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) { - Objects.requireNonNull(videoId); - - StoryboardRenderer renderer = null; - - List clientTypeList = List.of( - ClientType.ANDROID, - ClientType.ANDROID_VR - ); - - for (ClientType clientType : clientTypeList) { - renderer = getStoryboardRendererUsingBody(videoId, clientType, false); - if (renderer != null) break; - } - - if (renderer == null) { - Logger.printDebug(() -> videoId + " not available"); - } - - return renderer; - } -} \ No newline at end of file From 7fc164c2b53f515b1bebc0815e7a136222712a38 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 27 Aug 2024 00:55:54 -0400 Subject: [PATCH 18/91] fix: Comments --- .../revanced/integrations/youtube/patches/spoof/ClientType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java index a906f0739f..f5300cb87c 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java @@ -31,7 +31,7 @@ public enum ClientType { "Quest 3", "12", "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", - "32", // Android 12 SDK is version 31, but Quest 3 build.props uses version 32. + "32", // Android 12.1 "1.56.21" ); From 2e3f219087be6b7c0272f3e61f63f7fc45cc04aa Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 27 Aug 2024 01:24:20 -0400 Subject: [PATCH 19/91] Cleanup --- .../patches/spoof/SpoofClientPatch.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 5a5347cf45..4fb009bc80 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -8,6 +8,7 @@ import java.nio.ByteBuffer; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -89,14 +90,18 @@ public static boolean isSpoofingEnabled() { /** * Injection point. */ - public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Builder builder, - String url, Map playerHeaders) { + public static ExperimentalUrlRequest buildRequest(ExperimentalUrlRequest.Builder builder, + String url, Map playerHeaders) { if (SPOOF_CLIENT) { - Uri uri = Uri.parse(url); - String path = uri.getPath(); - if (path != null && path.contains("player") && !path.contains("heartbeat")) { - String videoId = uri.getQueryParameter("id"); - currentVideoStream = StreamingDataRequester.fetch(videoId, playerHeaders); + try { + Uri uri = Uri.parse(url); + String path = uri.getPath(); + if (path != null && path.contains("player") && !path.contains("heartbeat")) { + String videoId = Objects.requireNonNull(uri.getQueryParameter("id")); + currentVideoStream = StreamingDataRequester.fetch(videoId, playerHeaders); + } + } catch (Exception ex) { + Logger.printException(() -> "buildRequest failure", ex); } } @@ -106,7 +111,7 @@ public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Bu /** * Injection point. * Fix playback by replace the streaming data. - * Called after {@link #overrideUserAgent(ExperimentalUrlRequest.Builder, String, Map)}. + * Called after {@link #buildRequest(ExperimentalUrlRequest.Builder, String, Map)}. */ @Nullable public static ByteBuffer getStreamingData(String videoId) { From 4248422d01bedf2bfb896a345834563190a38571 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:37:09 -0400 Subject: [PATCH 20/91] fix: Can use non experimental parent class type --- .../youtube/patches/spoof/SpoofClientPatch.java | 9 +++++---- .../java/org/chromium/net/ExperimentalUrlRequest.java | 8 -------- stub/src/main/java/org/chromium/net/UrlRequest.java | 4 ++++ 3 files changed, 9 insertions(+), 12 deletions(-) delete mode 100644 stub/src/main/java/org/chromium/net/ExperimentalUrlRequest.java diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 4fb009bc80..a1372a0529 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -4,7 +4,7 @@ import androidx.annotation.Nullable; -import org.chromium.net.ExperimentalUrlRequest; +import org.chromium.net.UrlRequest; import java.nio.ByteBuffer; import java.util.Map; @@ -90,8 +90,8 @@ public static boolean isSpoofingEnabled() { /** * Injection point. */ - public static ExperimentalUrlRequest buildRequest(ExperimentalUrlRequest.Builder builder, - String url, Map playerHeaders) { + public static UrlRequest buildRequest(UrlRequest.Builder builder, String url, + Map playerHeaders) { if (SPOOF_CLIENT) { try { Uri uri = Uri.parse(url); @@ -111,7 +111,7 @@ public static ExperimentalUrlRequest buildRequest(ExperimentalUrlRequest.Builder /** * Injection point. * Fix playback by replace the streaming data. - * Called after {@link #buildRequest(ExperimentalUrlRequest.Builder, String, Map)}. + * Called after {@link #buildRequest(UrlRequest.Builder, String, Map)}. */ @Nullable public static ByteBuffer getStreamingData(String videoId) { @@ -147,6 +147,7 @@ public static ByteBuffer getStreamingData(String videoId) { * Injection point. * Called after {@link #getStreamingData(String)}. */ + @Nullable public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) { if (SPOOF_CLIENT) { try { diff --git a/stub/src/main/java/org/chromium/net/ExperimentalUrlRequest.java b/stub/src/main/java/org/chromium/net/ExperimentalUrlRequest.java deleted file mode 100644 index cdf2593e79..0000000000 --- a/stub/src/main/java/org/chromium/net/ExperimentalUrlRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.chromium.net; - -public abstract class ExperimentalUrlRequest { - public abstract class Builder { - public abstract ExperimentalUrlRequest.Builder addHeader(String name, String value); - public abstract ExperimentalUrlRequest build(); - } -} diff --git a/stub/src/main/java/org/chromium/net/UrlRequest.java b/stub/src/main/java/org/chromium/net/UrlRequest.java index 565fc22274..4c02f1a400 100644 --- a/stub/src/main/java/org/chromium/net/UrlRequest.java +++ b/stub/src/main/java/org/chromium/net/UrlRequest.java @@ -1,4 +1,8 @@ package org.chromium.net; public abstract class UrlRequest { + public abstract class Builder { + public abstract Builder addHeader(String name, String value); + public abstract UrlRequest build(); + } } From 760bc11ec5b1549c8ac1ab4a5298b17e8d13bb9e Mon Sep 17 00:00:00 2001 From: zainarbani Date: Sat, 31 Aug 2024 22:00:15 +0700 Subject: [PATCH 21/91] Don't block /get_watch --- .../patches/spoof/SpoofClientPatch.java | 68 ++++++++----------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index a1372a0529..74a25587f7 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -2,13 +2,13 @@ import android.net.Uri; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.chromium.net.UrlRequest; - import java.nio.ByteBuffer; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -27,34 +27,12 @@ public class SpoofClientPatch { * Any unreachable ip address. Used to intentionally fail requests. */ private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0"; - private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING); - - private static volatile Future currentVideoStream; - - /** - * Injection point. - * Blocks /get_watch requests by returning an unreachable URI. - * - * @param playerRequestUri The URI of the player request. - * @return An unreachable URI if the request is a /get_watch request, otherwise the original URI. - */ - public static Uri blockGetWatchRequest(Uri playerRequestUri) { - if (SPOOF_CLIENT) { - try { - String path = playerRequestUri.getPath(); - if (path != null && path.contains("get_watch")) { - Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri"); + private static volatile Map fetchHeaders; - return UNREACHABLE_HOST_URI; - } - } catch (Exception ex) { - Logger.printException(() -> "blockGetWatchRequest failure", ex); - } - } + private static volatile String lastFetchedVideoId; - return playerRequestUri; - } + private static final ConcurrentHashMap> streamingDataCache = new ConcurrentHashMap<>(); /** * Injection point. @@ -90,24 +68,32 @@ public static boolean isSpoofingEnabled() { /** * Injection point. */ - public static UrlRequest buildRequest(UrlRequest.Builder builder, String url, - Map playerHeaders) { + public static void setFetchHeaders(String url, Map headers) { if (SPOOF_CLIENT) { - try { - Uri uri = Uri.parse(url); - String path = uri.getPath(); - if (path != null && path.contains("player") && !path.contains("heartbeat")) { - String videoId = Objects.requireNonNull(uri.getQueryParameter("id")); - currentVideoStream = StreamingDataRequester.fetch(videoId, playerHeaders); - } - } catch (Exception ex) { - Logger.printException(() -> "buildRequest failure", ex); + Uri uri = Uri.parse(url); + String path = uri.getPath(); + if (path != null && path.contains("browse")) { + fetchHeaders = headers; } } - - return builder.build(); } + /** + * Injection point. + */ + public static void fetchStreamingData(@NonNull String videoId, boolean unused) { + if (SPOOF_CLIENT) { + if (videoId.equals(lastFetchedVideoId)) return; + + if (streamingDataCache.containsKey(videoId)) return; + + Future streamingData = StreamingDataRequester.fetch(videoId, fetchHeaders); + streamingDataCache.put(videoId, streamingData); + lastFetchedVideoId = videoId; + Logger.printDebug(() -> "Prefetch: " + videoId); + } + } + /** * Injection point. * Fix playback by replace the streaming data. @@ -119,7 +105,7 @@ public static ByteBuffer getStreamingData(String videoId) { try { Utils.verifyOffMainThread(); - var future = currentVideoStream; + var future = streamingDataCache.get(videoId); if (future != null) { final long maxSecondsToWait = 20; var stream = future.get(maxSecondsToWait, TimeUnit.SECONDS); From 3319922f7b0a7c4efbbd14872b358b0ec4eb5811 Mon Sep 17 00:00:00 2001 From: zainarbani Date: Sat, 31 Aug 2024 22:17:34 +0700 Subject: [PATCH 22/91] Remove debug --- .../integrations/youtube/patches/spoof/SpoofClientPatch.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 74a25587f7..8cffa0d375 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -90,7 +90,6 @@ public static void fetchStreamingData(@NonNull String videoId, boolean unused) { Future streamingData = StreamingDataRequester.fetch(videoId, fetchHeaders); streamingDataCache.put(videoId, streamingData); lastFetchedVideoId = videoId; - Logger.printDebug(() -> "Prefetch: " + videoId); } } From f46a46d93adde0e4f4d02d92f30940a9b4a696d5 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 31 Aug 2024 15:22:09 -0400 Subject: [PATCH 23/91] Update app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java Co-authored-by: kitadai31 <90122968+kitadai31@users.noreply.github.com> --- .../patches/spoof/DeviceHardwareSupport.java | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java index 3d2cb7911c..18c53a461b 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java @@ -8,10 +8,12 @@ import app.revanced.integrations.youtube.settings.Settings; public class DeviceHardwareSupport { - private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding(); - private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding(); + private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9; + private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1; - private static boolean deviceHasVP9HardwareDecoding() { + static { + boolean vp9found = false; + boolean av1found = false; MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { @@ -21,34 +23,18 @@ private static boolean deviceHasVP9HardwareDecoding() { if (isHardwareAccelerated && !codecInfo.isEncoder()) { for (String type : codecInfo.getSupportedTypes()) { if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { - Logger.printDebug(() -> "Device supports VP9 hardware decoding."); - return true; + vp9found = true; + } else if (type.equalsIgnoreCase("video/av01")) { + av1found = true; } } } } - - Logger.printDebug(() -> "Device does not support VP9 hardware decoding."); - return false; + + DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found; + DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found; } - private static boolean deviceHasAV1HardwareDecoding() { - // It appears all devices with hardware AV1 are also Android 10 or newer. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); - - for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) { - for (String type : codecInfo.getSupportedTypes()) { - if (type.equalsIgnoreCase("video/av01")) { - Logger.printDebug(() -> "Device supports AV1 hardware decoding."); - return true; - } - } - } - } - } - Logger.printDebug(() -> "Device does not support AV1 hardware decoding."); return false; } From 9010ba43c7a6501e0588f2d63d3b4997001ea3a8 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 31 Aug 2024 15:48:05 -0400 Subject: [PATCH 24/91] fix compilation --- .../youtube/patches/spoof/DeviceHardwareSupport.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java index 18c53a461b..8624f9e58e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java @@ -33,10 +33,13 @@ public class DeviceHardwareSupport { DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found; DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found; - } - Logger.printDebug(() -> "Device does not support AV1 hardware decoding."); - return false; + Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1 + ? "Device supports AV1 hardware decoding.\n" + : "Device does not support AV1 hardware decoding.\n" + + (DEVICE_HAS_HARDWARE_DECODING_VP9 + ? "Device supports VP9 hardware decoding." + : "Device does not support VP9 hardware decoding.")); } public static boolean allowVP9() { From f99572fa00c159f281ecc8799ef2bfa988f8bd96 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 31 Aug 2024 15:55:07 -0400 Subject: [PATCH 25/91] refactor: Can use a simpler synchronized map since there is little to no thread contention --- .../patches/spoof/SpoofClientPatch.java | 21 ++++++++++--------- .../java/org/chromium/net/UrlRequest.java | 4 ---- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 8cffa0d375..9ae7f76d08 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -6,6 +6,8 @@ import androidx.annotation.Nullable; import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @@ -30,9 +32,7 @@ public class SpoofClientPatch { private static volatile Map fetchHeaders; - private static volatile String lastFetchedVideoId; - - private static final ConcurrentHashMap> streamingDataCache = new ConcurrentHashMap<>(); + private static final Map> streamingDataCache = Collections.synchronizedMap(new HashMap<>()); /** * Injection point. @@ -83,20 +83,21 @@ public static void setFetchHeaders(String url, Map headers) { */ public static void fetchStreamingData(@NonNull String videoId, boolean unused) { if (SPOOF_CLIENT) { - if (videoId.equals(lastFetchedVideoId)) return; - - if (streamingDataCache.containsKey(videoId)) return; + try { + if (streamingDataCache.containsKey(videoId)) return; - Future streamingData = StreamingDataRequester.fetch(videoId, fetchHeaders); - streamingDataCache.put(videoId, streamingData); - lastFetchedVideoId = videoId; + Future streamingData = StreamingDataRequester.fetch(videoId, fetchHeaders); + streamingDataCache.put(videoId, streamingData); + } catch (Exception ex) { + Logger.printException(() -> "fetchStreamingData failure", ex); + } } } /** * Injection point. * Fix playback by replace the streaming data. - * Called after {@link #buildRequest(UrlRequest.Builder, String, Map)}. + * Called after {@link #setFetchHeaders(String, Map)} . */ @Nullable public static ByteBuffer getStreamingData(String videoId) { diff --git a/stub/src/main/java/org/chromium/net/UrlRequest.java b/stub/src/main/java/org/chromium/net/UrlRequest.java index 4c02f1a400..565fc22274 100644 --- a/stub/src/main/java/org/chromium/net/UrlRequest.java +++ b/stub/src/main/java/org/chromium/net/UrlRequest.java @@ -1,8 +1,4 @@ package org.chromium.net; public abstract class UrlRequest { - public abstract class Builder { - public abstract Builder addHeader(String name, String value); - public abstract UrlRequest build(); - } } From e5a6981921628cbd76100a258487de641599344d Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 31 Aug 2024 16:10:26 -0400 Subject: [PATCH 26/91] fix: Do not fetch Shorts streams until the Short is opened --- .../youtube/patches/spoof/SpoofClientPatch.java | 13 ++++++++++--- .../spoof/requests/StreamingDataRequester.java | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 9ae7f76d08..50bea64d84 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -9,8 +9,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -18,6 +16,7 @@ import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.youtube.patches.VideoInformation; import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequester; import app.revanced.integrations.youtube.settings.Settings; @@ -81,9 +80,17 @@ public static void setFetchHeaders(String url, Map headers) { /** * Injection point. */ - public static void fetchStreamingData(@NonNull String videoId, boolean unused) { + public static void fetchStreamingData(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) { if (SPOOF_CLIENT) { try { + final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort(); + // Shorts shelf in home and subscription feed causes player response hook to be called, + // and the 'is opening/playing' parameter will be false. + // This hook will be called again when the Short is actually opened. + if (videoIdIsShort && !isShortAndOpeningOrPlaying) { + return; + } + if (streamingDataCache.containsKey(videoId)) return; Future streamingData = StreamingDataRequester.fetch(videoId, fetchHeaders); diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java index 113fd264f8..14d93f9dcb 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java @@ -37,7 +37,7 @@ private static HttpURLConnection send(ClientType clientType, String videoId, boolean showErrorToasts) { final long startTime = System.currentTimeMillis(); String clientTypeName = clientType.name(); - Logger.printDebug(() -> "Fetching video streams using client: " + clientType.name()); + Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name()); try { HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); @@ -65,7 +65,7 @@ private static HttpURLConnection send(ClientType clientType, String videoId, } catch (Exception ex) { Logger.printException(() -> "send failed", ex); } finally { - Logger.printDebug(() -> clientTypeName + " took: " + (System.currentTimeMillis() - startTime) + "ms"); + Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms"); } return null; From 397f5cf80b00416309d8b86b49fccd17be738055 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 31 Aug 2024 16:50:35 -0400 Subject: [PATCH 27/91] refactor --- .../patches/spoof/SpoofClientPatch.java | 44 ++-- .../spoof/requests/StreamingDataRequest.java | 211 ++++++++++++++++++ .../requests/StreamingDataRequester.java | 124 ---------- 3 files changed, 227 insertions(+), 152 deletions(-) create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 50bea64d84..3d39339447 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -6,18 +6,12 @@ import androidx.annotation.Nullable; import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.patches.VideoInformation; -import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequester; +import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequest; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") @@ -31,7 +25,6 @@ public class SpoofClientPatch { private static volatile Map fetchHeaders; - private static final Map> streamingDataCache = Collections.synchronizedMap(new HashMap<>()); /** * Injection point. @@ -69,10 +62,14 @@ public static boolean isSpoofingEnabled() { */ public static void setFetchHeaders(String url, Map headers) { if (SPOOF_CLIENT) { - Uri uri = Uri.parse(url); - String path = uri.getPath(); - if (path != null && path.contains("browse")) { - fetchHeaders = headers; + try { + Uri uri = Uri.parse(url); + String path = uri.getPath(); + if (path != null && path.contains("browse")) { + fetchHeaders = headers; + } + } catch (Exception ex) { + Logger.printException(() -> "setFetchHeaders failure", ex); } } } @@ -91,10 +88,7 @@ public static void fetchStreamingData(@NonNull String videoId, boolean isShortAn return; } - if (streamingDataCache.containsKey(videoId)) return; - - Future streamingData = StreamingDataRequester.fetch(videoId, fetchHeaders); - streamingDataCache.put(videoId, streamingData); + StreamingDataRequest.fetchRequestIfNeeded(videoId, fetchHeaders); } catch (Exception ex) { Logger.printException(() -> "fetchStreamingData failure", ex); } @@ -112,23 +106,17 @@ public static ByteBuffer getStreamingData(String videoId) { try { Utils.verifyOffMainThread(); - var future = streamingDataCache.get(videoId); - if (future != null) { - final long maxSecondsToWait = 20; - var stream = future.get(maxSecondsToWait, TimeUnit.SECONDS); + StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId); + if (request != null) { + var stream = request.getStream(); if (stream != null) { Logger.printDebug(() -> "Overriding video stream"); return stream; } - - Logger.printDebug(() -> "Not overriding streaming data (video stream is null)"); } - } catch (TimeoutException ex) { - Logger.printInfo(() -> "getStreamingData timed out", ex); - } catch (InterruptedException ex) { - Logger.printException(() -> "getStreamingData interrupted", ex); - Thread.currentThread().interrupt(); // Restore interrupt status flag. - } catch (ExecutionException ex) { + + Logger.printDebug(() -> "Not overriding streaming data (video stream is null)"); + } catch (Exception ex) { Logger.printException(() -> "getStreamingData failure", ex); } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java new file mode 100644 index 0000000000..cb7091c8de --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java @@ -0,0 +1,211 @@ +package app.revanced.integrations.youtube.patches.spoof.requests; + +import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA; + +import android.os.Build; + +import androidx.annotation.GuardedBy; +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.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.shared.settings.BaseSettings; +import app.revanced.integrations.youtube.patches.spoof.ClientType; + +public class StreamingDataRequest { + + /** + * How long to keep fetches until they are expired. + */ + private static final long CACHE_RETENTION_TIME_MILLISECONDS = 60 * 1000; // 1 Minute + + private static final long MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; // 20 seconds + + @GuardedBy("itself") + private static final Map cache = new HashMap<>(); + + public static void fetchRequestIfNeeded(@Nullable String videoId, Map fetchHeaders) { + Objects.requireNonNull(videoId); + synchronized (cache) { + // Remove any expired entries. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + final long now = System.currentTimeMillis(); + cache.values().removeIf(request -> { + final boolean expired = request.isExpired(now); + if (expired) Logger.printDebug(() -> "Removing expired stream: " + request.videoId); + return expired; + }); + } + + if (!cache.containsKey(videoId)) { + cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); + } + } + } + + @Nullable + public static StreamingDataRequest getRequestForVideoId(@Nullable String videoId) { + synchronized (cache) { + return cache.get(videoId); + } + } + + private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) { + if (showToast) Utils.showToastShort(toastMessage); + Logger.printInfo(() -> toastMessage, ex); + } + + @Nullable + private static HttpURLConnection send(ClientType clientType, String videoId, + Map playerHeaders, + boolean showErrorToasts) { + Objects.requireNonNull(clientType); + Objects.requireNonNull(videoId); + Objects.requireNonNull(playerHeaders); + + final long startTime = System.currentTimeMillis(); + String clientTypeName = clientType.name(); + Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name()); + + try { + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); + + String authHeader = playerHeaders.get("Authorization"); + String visitorId = playerHeaders.get("X-Goog-Visitor-Id"); + connection.setRequestProperty("Authorization", authHeader); + connection.setRequestProperty("X-Goog-Visitor-Id", visitorId); + + String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); + byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(requestBody.length); + connection.getOutputStream().write(requestBody); + + final int responseCode = connection.getResponseCode(); + if (responseCode == 200) return connection; + + handleConnectionError(clientTypeName + " not available with response code: " + + responseCode + " message: " + connection.getResponseMessage(), + null, showErrorToasts); + } catch (SocketTimeoutException ex) { + handleConnectionError("Connection timeout", ex, showErrorToasts); + } catch (IOException ex) { + handleConnectionError("Network error", ex, showErrorToasts); + } catch (Exception ex) { + Logger.printException(() -> "send failed", ex); + } finally { + Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms"); + } + + return null; + } + + private static ByteBuffer fetch(@NonNull String videoId, Map playerHeaders) { + // Retry with different client if empty response body is received. + ClientType[] clientTypesToUse = { + ClientType.IOS, + ClientType.ANDROID_VR + }; + + final boolean debugEnabled = BaseSettings.DEBUG.get(); + if (debugEnabled) { + // To ensure the different clients are used while debugging, + // use a random client order. + Collections.shuffle(Arrays.asList(clientTypesToUse)); + } + + int i = 0; + for (ClientType clientType : clientTypesToUse) { + // Show an error if the last client type fails, or if the debug is enabled then show for all attempts. + final boolean showErrorToast = (++i == clientTypesToUse.length) || debugEnabled; + + 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) { + try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) >= 0) { + baos.write(buffer, 0, bytesRead); + } + + return ByteBuffer.wrap(baos.toByteArray()); + } + } + } + } catch (IOException ex) { + Logger.printException(() -> "Fetch failed while processing response data", ex); + } + } + } + + handleConnectionError("Could not fetch any client streams", null, debugEnabled); + return null; + } + + + /** + * Time this instance and the fetch future was created. + */ + private final long timeFetched; + private final String videoId; + private final Future future; + + private StreamingDataRequest(String videoId, Map playerHeaders) { + Objects.requireNonNull(playerHeaders); + this.timeFetched = System.currentTimeMillis(); + this.videoId = videoId; + this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders)); + } + + public boolean isExpired(long now) { + final long timeSinceCreation = now - timeFetched; + if (timeSinceCreation > CACHE_RETENTION_TIME_MILLISECONDS) { + return true; + } + + // Only expired if the fetch failed (API null response). + return (fetchCompleted() && getStream() == null); + } + + /** + * @return if the RYD fetch call has completed. + */ + public boolean fetchCompleted() { + return future.isDone(); + } + + @Nullable + public ByteBuffer getStream() { + try { + return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS); + } catch (TimeoutException ex) { + Logger.printInfo(() -> "getStream timed out", ex); + } catch (InterruptedException ex) { + Logger.printException(() -> "getStream interrupted", ex); + Thread.currentThread().interrupt(); // Restore interrupt status flag. + } catch (ExecutionException ex) { + Logger.printException(() -> "getStream failure", ex); + } + + return null; + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java deleted file mode 100644 index 14d93f9dcb..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java +++ /dev/null @@ -1,124 +0,0 @@ -package app.revanced.integrations.youtube.patches.spoof.requests; - -import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.GET_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.*; -import java.util.concurrent.Future; - -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; -import app.revanced.integrations.shared.settings.BaseSettings; -import app.revanced.integrations.youtube.patches.spoof.ClientType; - -public class StreamingDataRequester { - - private StreamingDataRequester() { - } - - private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) { - if (showToast) Utils.showToastShort(toastMessage); - Logger.printInfo(() -> toastMessage, ex); - } - - @Nullable - private static HttpURLConnection send(ClientType clientType, String videoId, - Map playerHeaders, - boolean showErrorToasts) { - final long startTime = System.currentTimeMillis(); - String clientTypeName = clientType.name(); - Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name()); - - try { - HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); - - String authHeader = playerHeaders.get("Authorization"); - String visitorId = playerHeaders.get("X-Goog-Visitor-Id"); - connection.setRequestProperty("Authorization", authHeader); - connection.setRequestProperty("X-Goog-Visitor-Id", visitorId); - - String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); - byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); - connection.setFixedLengthStreamingMode(requestBody.length); - connection.getOutputStream().write(requestBody); - - final int responseCode = connection.getResponseCode(); - if (responseCode == 200) return connection; - - handleConnectionError(clientTypeName + " not available with response code: " - + responseCode + " message: " + connection.getResponseMessage(), - null, showErrorToasts); - } catch (SocketTimeoutException ex) { - handleConnectionError("Connection timeout", ex, showErrorToasts); - } catch (IOException ex) { - handleConnectionError("Network error", ex, showErrorToasts); - } catch (Exception ex) { - Logger.printException(() -> "send failed", ex); - } finally { - Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms"); - } - - return null; - } - - public static Future fetch(@NonNull String videoId, Map playerHeaders) { - Objects.requireNonNull(videoId); - - return Utils.submitOnBackgroundThread(() -> { - // Retry with different client if empty response body is received. - ClientType[] clientTypesToUse = { - ClientType.IOS, - ClientType.ANDROID_VR - }; - - final boolean debugEnabled = BaseSettings.DEBUG.get(); - if (debugEnabled) { - // To ensure the different clients are used while debugging, - // use a random client order. - Collections.shuffle(Arrays.asList(clientTypesToUse)); - } - - int i = 0; - for (ClientType clientType : clientTypesToUse) { - // Show an error if the last client type fails, or if the debug is enabled then show for all attempts. - final boolean showErrorToast = (++i == clientTypesToUse.length) || debugEnabled; - - 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) { - try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - byte[] buffer = new byte[8192]; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) >= 0) { - baos.write(buffer, 0, bytesRead); - } - - return ByteBuffer.wrap(baos.toByteArray()); - } - } - } - } catch (IOException ex) { - Logger.printException(() -> "Fetch failed while processing response data", ex); - } - } - } - - handleConnectionError("Could not fetch any client streams", null, debugEnabled); - return null; - }); - } -} From 1a3086822d803913c721d3052892959958fbcb35 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 31 Aug 2024 16:54:42 -0400 Subject: [PATCH 28/91] refactor --- .../youtube/patches/spoof/SpoofClientPatch.java | 8 ++------ .../patches/spoof/requests/StreamingDataRequest.java | 2 ++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 3d39339447..fd2461e19e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -9,7 +9,6 @@ import java.util.Map; import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.patches.VideoInformation; import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequest; import app.revanced.integrations.youtube.settings.Settings; @@ -25,7 +24,6 @@ public class SpoofClientPatch { private static volatile Map fetchHeaders; - /** * Injection point. *

@@ -104,18 +102,16 @@ public static void fetchStreamingData(@NonNull String videoId, boolean isShortAn public static ByteBuffer getStreamingData(String videoId) { if (SPOOF_CLIENT) { try { - Utils.verifyOffMainThread(); - StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId); if (request != null) { var stream = request.getStream(); if (stream != null) { - Logger.printDebug(() -> "Overriding video stream"); + Logger.printDebug(() -> "Overriding video stream: " + videoId); return stream; } } - Logger.printDebug(() -> "Not overriding streaming data (video stream is null)"); + Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId); } catch (Exception ex) { Logger.printException(() -> "getStreamingData failure", ex); } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java index cb7091c8de..6f9e567dac 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java @@ -195,6 +195,8 @@ public boolean fetchCompleted() { @Nullable public ByteBuffer getStream() { + Utils.verifyOffMainThread(); + try { return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS); } catch (TimeoutException ex) { From 6c24707de72ac0679d62f4966f940e4c2329d179 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 31 Aug 2024 19:21:54 -0400 Subject: [PATCH 29/91] refactor --- .../youtube/patches/spoof/DeviceHardwareSupport.java | 11 ++++++----- .../youtube/patches/spoof/SpoofClientPatch.java | 3 +++ .../patches/spoof/requests/StreamingDataRequest.java | 2 -- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java index 8624f9e58e..64f42ae39d 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java @@ -15,9 +15,10 @@ public class DeviceHardwareSupport { boolean vp9found = false; boolean av1found = false; MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + final boolean deviceIsAndroidTenOrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + final boolean isHardwareAccelerated = deviceIsAndroidTenOrLater ? codecInfo.isHardwareAccelerated() : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. if (isHardwareAccelerated && !codecInfo.isEncoder()) { @@ -35,11 +36,11 @@ public class DeviceHardwareSupport { DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found; Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1 - ? "Device supports AV1 hardware decoding.\n" - : "Device does not support AV1 hardware decoding.\n" + ? "Device supports AV1 hardware decoding\n" + : "Device does not support AV1 hardware decoding\n" + (DEVICE_HAS_HARDWARE_DECODING_VP9 - ? "Device supports VP9 hardware decoding." - : "Device does not support VP9 hardware decoding.")); + ? "Device supports VP9 hardware decoding" + : "Device does not support VP9 hardware decoding")); } public static boolean allowVP9() { diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index fd2461e19e..abeb1bd21e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -9,6 +9,7 @@ import java.util.Map; import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.patches.VideoInformation; import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequest; import app.revanced.integrations.youtube.settings.Settings; @@ -102,6 +103,8 @@ public static void fetchStreamingData(@NonNull String videoId, boolean isShortAn public static ByteBuffer getStreamingData(String videoId) { if (SPOOF_CLIENT) { try { + Utils.verifyOffMainThread(); + StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId); if (request != null) { var stream = request.getStream(); diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java index 6f9e567dac..cb7091c8de 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java @@ -195,8 +195,6 @@ public boolean fetchCompleted() { @Nullable public ByteBuffer getStream() { - Utils.verifyOffMainThread(); - try { return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS); } catch (TimeoutException ex) { From fa818321bfd3be5658bc1bf5b20ecd6fb991ee9d Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 3 Sep 2024 00:21:53 -0400 Subject: [PATCH 30/91] Revert "Don't block /get_watch" --- .../patches/spoof/DeviceHardwareSupport.java | 50 +++-- .../patches/spoof/SpoofClientPatch.java | 94 +++++--- .../spoof/requests/StreamingDataRequest.java | 211 ------------------ .../requests/StreamingDataRequester.java | 124 ++++++++++ .../java/org/chromium/net/UrlRequest.java | 4 + 5 files changed, 215 insertions(+), 268 deletions(-) delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java index 64f42ae39d..3d2cb7911c 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java @@ -8,39 +8,49 @@ import app.revanced.integrations.youtube.settings.Settings; public class DeviceHardwareSupport { - private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9; - private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1; + private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding(); + private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding(); - static { - boolean vp9found = false; - boolean av1found = false; + private static boolean deviceHasVP9HardwareDecoding() { MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); - final boolean deviceIsAndroidTenOrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - final boolean isHardwareAccelerated = deviceIsAndroidTenOrLater + final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ? codecInfo.isHardwareAccelerated() : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. if (isHardwareAccelerated && !codecInfo.isEncoder()) { for (String type : codecInfo.getSupportedTypes()) { if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { - vp9found = true; - } else if (type.equalsIgnoreCase("video/av01")) { - av1found = true; + Logger.printDebug(() -> "Device supports VP9 hardware decoding."); + return true; } } } } - - DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found; - DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found; - - Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1 - ? "Device supports AV1 hardware decoding\n" - : "Device does not support AV1 hardware decoding\n" - + (DEVICE_HAS_HARDWARE_DECODING_VP9 - ? "Device supports VP9 hardware decoding" - : "Device does not support VP9 hardware decoding")); + + Logger.printDebug(() -> "Device does not support VP9 hardware decoding."); + return false; + } + + private static boolean deviceHasAV1HardwareDecoding() { + // It appears all devices with hardware AV1 are also Android 10 or newer. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + + for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { + if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) { + for (String type : codecInfo.getSupportedTypes()) { + if (type.equalsIgnoreCase("video/av01")) { + Logger.printDebug(() -> "Device supports AV1 hardware decoding."); + return true; + } + } + } + } + } + + Logger.printDebug(() -> "Device does not support AV1 hardware decoding."); + return false; } public static boolean allowVP9() { diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index abeb1bd21e..a1372a0529 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -2,16 +2,21 @@ import android.net.Uri; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.chromium.net.UrlRequest; + import java.nio.ByteBuffer; import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; -import app.revanced.integrations.youtube.patches.VideoInformation; -import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequest; +import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequester; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") @@ -22,8 +27,34 @@ public class SpoofClientPatch { * Any unreachable ip address. Used to intentionally fail requests. */ private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0"; + private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING); + + private static volatile Future currentVideoStream; + + /** + * Injection point. + * Blocks /get_watch requests by returning an unreachable URI. + * + * @param playerRequestUri The URI of the player request. + * @return An unreachable URI if the request is a /get_watch request, otherwise the original URI. + */ + public static Uri blockGetWatchRequest(Uri playerRequestUri) { + if (SPOOF_CLIENT) { + try { + String path = playerRequestUri.getPath(); - private static volatile Map fetchHeaders; + if (path != null && path.contains("get_watch")) { + Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri"); + + return UNREACHABLE_HOST_URI; + } + } catch (Exception ex) { + Logger.printException(() -> "blockGetWatchRequest failure", ex); + } + } + + return playerRequestUri; + } /** * Injection point. @@ -59,45 +90,28 @@ public static boolean isSpoofingEnabled() { /** * Injection point. */ - public static void setFetchHeaders(String url, Map headers) { + public static UrlRequest buildRequest(UrlRequest.Builder builder, String url, + Map playerHeaders) { if (SPOOF_CLIENT) { try { Uri uri = Uri.parse(url); String path = uri.getPath(); - if (path != null && path.contains("browse")) { - fetchHeaders = headers; + if (path != null && path.contains("player") && !path.contains("heartbeat")) { + String videoId = Objects.requireNonNull(uri.getQueryParameter("id")); + currentVideoStream = StreamingDataRequester.fetch(videoId, playerHeaders); } } catch (Exception ex) { - Logger.printException(() -> "setFetchHeaders failure", ex); + Logger.printException(() -> "buildRequest failure", ex); } } - } - - /** - * Injection point. - */ - public static void fetchStreamingData(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) { - if (SPOOF_CLIENT) { - try { - final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort(); - // Shorts shelf in home and subscription feed causes player response hook to be called, - // and the 'is opening/playing' parameter will be false. - // This hook will be called again when the Short is actually opened. - if (videoIdIsShort && !isShortAndOpeningOrPlaying) { - return; - } - StreamingDataRequest.fetchRequestIfNeeded(videoId, fetchHeaders); - } catch (Exception ex) { - Logger.printException(() -> "fetchStreamingData failure", ex); - } - } - } + return builder.build(); + } /** * Injection point. * Fix playback by replace the streaming data. - * Called after {@link #setFetchHeaders(String, Map)} . + * Called after {@link #buildRequest(UrlRequest.Builder, String, Map)}. */ @Nullable public static ByteBuffer getStreamingData(String videoId) { @@ -105,17 +119,23 @@ public static ByteBuffer getStreamingData(String videoId) { try { Utils.verifyOffMainThread(); - StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId); - if (request != null) { - var stream = request.getStream(); + var future = currentVideoStream; + if (future != null) { + final long maxSecondsToWait = 20; + var stream = future.get(maxSecondsToWait, TimeUnit.SECONDS); if (stream != null) { - Logger.printDebug(() -> "Overriding video stream: " + videoId); + Logger.printDebug(() -> "Overriding video stream"); return stream; } - } - Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId); - } catch (Exception ex) { + Logger.printDebug(() -> "Not overriding streaming data (video stream is null)"); + } + } catch (TimeoutException ex) { + Logger.printInfo(() -> "getStreamingData timed out", ex); + } catch (InterruptedException ex) { + Logger.printException(() -> "getStreamingData interrupted", ex); + Thread.currentThread().interrupt(); // Restore interrupt status flag. + } catch (ExecutionException ex) { Logger.printException(() -> "getStreamingData failure", ex); } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java deleted file mode 100644 index cb7091c8de..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java +++ /dev/null @@ -1,211 +0,0 @@ -package app.revanced.integrations.youtube.patches.spoof.requests; - -import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA; - -import android.os.Build; - -import androidx.annotation.GuardedBy; -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.*; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; -import app.revanced.integrations.shared.settings.BaseSettings; -import app.revanced.integrations.youtube.patches.spoof.ClientType; - -public class StreamingDataRequest { - - /** - * How long to keep fetches until they are expired. - */ - private static final long CACHE_RETENTION_TIME_MILLISECONDS = 60 * 1000; // 1 Minute - - private static final long MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; // 20 seconds - - @GuardedBy("itself") - private static final Map cache = new HashMap<>(); - - public static void fetchRequestIfNeeded(@Nullable String videoId, Map fetchHeaders) { - Objects.requireNonNull(videoId); - synchronized (cache) { - // Remove any expired entries. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - final long now = System.currentTimeMillis(); - cache.values().removeIf(request -> { - final boolean expired = request.isExpired(now); - if (expired) Logger.printDebug(() -> "Removing expired stream: " + request.videoId); - return expired; - }); - } - - if (!cache.containsKey(videoId)) { - cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); - } - } - } - - @Nullable - public static StreamingDataRequest getRequestForVideoId(@Nullable String videoId) { - synchronized (cache) { - return cache.get(videoId); - } - } - - private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) { - if (showToast) Utils.showToastShort(toastMessage); - Logger.printInfo(() -> toastMessage, ex); - } - - @Nullable - private static HttpURLConnection send(ClientType clientType, String videoId, - Map playerHeaders, - boolean showErrorToasts) { - Objects.requireNonNull(clientType); - Objects.requireNonNull(videoId); - Objects.requireNonNull(playerHeaders); - - final long startTime = System.currentTimeMillis(); - String clientTypeName = clientType.name(); - Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name()); - - try { - HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); - - String authHeader = playerHeaders.get("Authorization"); - String visitorId = playerHeaders.get("X-Goog-Visitor-Id"); - connection.setRequestProperty("Authorization", authHeader); - connection.setRequestProperty("X-Goog-Visitor-Id", visitorId); - - String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); - byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); - connection.setFixedLengthStreamingMode(requestBody.length); - connection.getOutputStream().write(requestBody); - - final int responseCode = connection.getResponseCode(); - if (responseCode == 200) return connection; - - handleConnectionError(clientTypeName + " not available with response code: " - + responseCode + " message: " + connection.getResponseMessage(), - null, showErrorToasts); - } catch (SocketTimeoutException ex) { - handleConnectionError("Connection timeout", ex, showErrorToasts); - } catch (IOException ex) { - handleConnectionError("Network error", ex, showErrorToasts); - } catch (Exception ex) { - Logger.printException(() -> "send failed", ex); - } finally { - Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms"); - } - - return null; - } - - private static ByteBuffer fetch(@NonNull String videoId, Map playerHeaders) { - // Retry with different client if empty response body is received. - ClientType[] clientTypesToUse = { - ClientType.IOS, - ClientType.ANDROID_VR - }; - - final boolean debugEnabled = BaseSettings.DEBUG.get(); - if (debugEnabled) { - // To ensure the different clients are used while debugging, - // use a random client order. - Collections.shuffle(Arrays.asList(clientTypesToUse)); - } - - int i = 0; - for (ClientType clientType : clientTypesToUse) { - // Show an error if the last client type fails, or if the debug is enabled then show for all attempts. - final boolean showErrorToast = (++i == clientTypesToUse.length) || debugEnabled; - - 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) { - try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - byte[] buffer = new byte[8192]; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) >= 0) { - baos.write(buffer, 0, bytesRead); - } - - return ByteBuffer.wrap(baos.toByteArray()); - } - } - } - } catch (IOException ex) { - Logger.printException(() -> "Fetch failed while processing response data", ex); - } - } - } - - handleConnectionError("Could not fetch any client streams", null, debugEnabled); - return null; - } - - - /** - * Time this instance and the fetch future was created. - */ - private final long timeFetched; - private final String videoId; - private final Future future; - - private StreamingDataRequest(String videoId, Map playerHeaders) { - Objects.requireNonNull(playerHeaders); - this.timeFetched = System.currentTimeMillis(); - this.videoId = videoId; - this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders)); - } - - public boolean isExpired(long now) { - final long timeSinceCreation = now - timeFetched; - if (timeSinceCreation > CACHE_RETENTION_TIME_MILLISECONDS) { - return true; - } - - // Only expired if the fetch failed (API null response). - return (fetchCompleted() && getStream() == null); - } - - /** - * @return if the RYD fetch call has completed. - */ - public boolean fetchCompleted() { - return future.isDone(); - } - - @Nullable - public ByteBuffer getStream() { - try { - return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS); - } catch (TimeoutException ex) { - Logger.printInfo(() -> "getStream timed out", ex); - } catch (InterruptedException ex) { - Logger.printException(() -> "getStream interrupted", ex); - Thread.currentThread().interrupt(); // Restore interrupt status flag. - } catch (ExecutionException ex) { - Logger.printException(() -> "getStream failure", ex); - } - - return null; - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java new file mode 100644 index 0000000000..113fd264f8 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java @@ -0,0 +1,124 @@ +package app.revanced.integrations.youtube.patches.spoof.requests; + +import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.GET_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.*; +import java.util.concurrent.Future; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.shared.settings.BaseSettings; +import app.revanced.integrations.youtube.patches.spoof.ClientType; + +public class StreamingDataRequester { + + private StreamingDataRequester() { + } + + private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) { + if (showToast) Utils.showToastShort(toastMessage); + Logger.printInfo(() -> toastMessage, ex); + } + + @Nullable + private static HttpURLConnection send(ClientType clientType, String videoId, + Map playerHeaders, + boolean showErrorToasts) { + final long startTime = System.currentTimeMillis(); + String clientTypeName = clientType.name(); + Logger.printDebug(() -> "Fetching video streams using client: " + clientType.name()); + + try { + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); + + String authHeader = playerHeaders.get("Authorization"); + String visitorId = playerHeaders.get("X-Goog-Visitor-Id"); + connection.setRequestProperty("Authorization", authHeader); + connection.setRequestProperty("X-Goog-Visitor-Id", visitorId); + + String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); + byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(requestBody.length); + connection.getOutputStream().write(requestBody); + + final int responseCode = connection.getResponseCode(); + if (responseCode == 200) return connection; + + handleConnectionError(clientTypeName + " not available with response code: " + + responseCode + " message: " + connection.getResponseMessage(), + null, showErrorToasts); + } catch (SocketTimeoutException ex) { + handleConnectionError("Connection timeout", ex, showErrorToasts); + } catch (IOException ex) { + handleConnectionError("Network error", ex, showErrorToasts); + } catch (Exception ex) { + Logger.printException(() -> "send failed", ex); + } finally { + Logger.printDebug(() -> clientTypeName + " took: " + (System.currentTimeMillis() - startTime) + "ms"); + } + + return null; + } + + public static Future fetch(@NonNull String videoId, Map playerHeaders) { + Objects.requireNonNull(videoId); + + return Utils.submitOnBackgroundThread(() -> { + // Retry with different client if empty response body is received. + ClientType[] clientTypesToUse = { + ClientType.IOS, + ClientType.ANDROID_VR + }; + + final boolean debugEnabled = BaseSettings.DEBUG.get(); + if (debugEnabled) { + // To ensure the different clients are used while debugging, + // use a random client order. + Collections.shuffle(Arrays.asList(clientTypesToUse)); + } + + int i = 0; + for (ClientType clientType : clientTypesToUse) { + // Show an error if the last client type fails, or if the debug is enabled then show for all attempts. + final boolean showErrorToast = (++i == clientTypesToUse.length) || debugEnabled; + + 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) { + try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) >= 0) { + baos.write(buffer, 0, bytesRead); + } + + return ByteBuffer.wrap(baos.toByteArray()); + } + } + } + } catch (IOException ex) { + Logger.printException(() -> "Fetch failed while processing response data", ex); + } + } + } + + handleConnectionError("Could not fetch any client streams", null, debugEnabled); + return null; + }); + } +} diff --git a/stub/src/main/java/org/chromium/net/UrlRequest.java b/stub/src/main/java/org/chromium/net/UrlRequest.java index 565fc22274..4c02f1a400 100644 --- a/stub/src/main/java/org/chromium/net/UrlRequest.java +++ b/stub/src/main/java/org/chromium/net/UrlRequest.java @@ -1,4 +1,8 @@ package org.chromium.net; public abstract class UrlRequest { + public abstract class Builder { + public abstract Builder addHeader(String name, String value); + public abstract UrlRequest build(); + } } From 2d7ea04a0658d63d6809bc0e6d4377ee80c6cf09 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 3 Sep 2024 00:23:42 -0400 Subject: [PATCH 31/91] refactor --- .../patches/spoof/DeviceHardwareSupport.java | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java index 3d2cb7911c..2eead95753 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java @@ -8,49 +8,39 @@ import app.revanced.integrations.youtube.settings.Settings; public class DeviceHardwareSupport { - private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding(); - private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding(); + private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9; + private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1; - private static boolean deviceHasVP9HardwareDecoding() { + static { + boolean vp9found = false; + boolean av1found = false; MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + final boolean deviceIsAndroidTenOrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + final boolean isHardwareAccelerated = deviceIsAndroidTenOrLater ? codecInfo.isHardwareAccelerated() : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. if (isHardwareAccelerated && !codecInfo.isEncoder()) { for (String type : codecInfo.getSupportedTypes()) { if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { - Logger.printDebug(() -> "Device supports VP9 hardware decoding."); - return true; + vp9found = true; + } else if (type.equalsIgnoreCase("video/av01")) { + av1found = true; } } } } - Logger.printDebug(() -> "Device does not support VP9 hardware decoding."); - return false; - } - - private static boolean deviceHasAV1HardwareDecoding() { - // It appears all devices with hardware AV1 are also Android 10 or newer. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); - - for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) { - for (String type : codecInfo.getSupportedTypes()) { - if (type.equalsIgnoreCase("video/av01")) { - Logger.printDebug(() -> "Device supports AV1 hardware decoding."); - return true; - } - } - } - } - } + DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found; + DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found; - Logger.printDebug(() -> "Device does not support AV1 hardware decoding."); - return false; + Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1 + ? "Device supports AV1 hardware decoding\n" + : "Device does not support AV1 hardware decoding\n" + + (DEVICE_HAS_HARDWARE_DECODING_VP9 + ? "Device supports VP9 hardware decoding" + : "Device does not support VP9 hardware decoding")); } public static boolean allowVP9() { From 55c7ab878f5ed3c79a453058de99a9ecc312a568 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 7 Sep 2024 07:06:29 -0400 Subject: [PATCH 32/91] fix: Remove assert until a way around it is figured out --- .../integrations/youtube/patches/spoof/SpoofClientPatch.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index a1372a0529..3a5f9168ff 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -117,7 +117,9 @@ public static UrlRequest buildRequest(UrlRequest.Builder builder, String url, public static ByteBuffer getStreamingData(String videoId) { if (SPOOF_CLIENT) { try { - Utils.verifyOffMainThread(); + // FIXME: Sometimes this is called on the main thread (such as when a Shorts advertisement loads) + // But this method should be called off the main thread since it blocks on a network request. + // Utils.verifyOffMainThread(); // TODO: figure out what to change to enable this. var future = currentVideoStream; if (future != null) { From bb453162cc74b8acd076ee783ca9301891de0a6a Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 7 Sep 2024 16:40:50 -0400 Subject: [PATCH 33/91] Comments, logging --- .../youtube/patches/spoof/SpoofClientPatch.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 3a5f9168ff..74532f0451 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -15,7 +15,6 @@ import java.util.concurrent.TimeoutException; import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequester; import app.revanced.integrations.youtube.settings.Settings; @@ -117,23 +116,24 @@ public static UrlRequest buildRequest(UrlRequest.Builder builder, String url, public static ByteBuffer getStreamingData(String videoId) { if (SPOOF_CLIENT) { try { - // FIXME: Sometimes this is called on the main thread (such as when a Shorts advertisement loads) - // But this method should be called off the main thread since it blocks on a network request. - // Utils.verifyOffMainThread(); // TODO: figure out what to change to enable this. + // This hook is always called off the main thread, + // but can be later called for the same video id on the main thread. + // This is not a concern, since the main thread call will always be + // finished and never block. var future = currentVideoStream; if (future != null) { final long maxSecondsToWait = 20; var stream = future.get(maxSecondsToWait, TimeUnit.SECONDS); if (stream != null) { - Logger.printDebug(() -> "Overriding video stream"); + Logger.printDebug(() -> "Overriding video stream: " + videoId); return stream; } - Logger.printDebug(() -> "Not overriding streaming data (video stream is null)"); + Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId); } } catch (TimeoutException ex) { - Logger.printInfo(() -> "getStreamingData timed out", ex); + Logger.printInfo(() -> "getStreamingData timed out: " + videoId, ex); } catch (InterruptedException ex) { Logger.printException(() -> "getStreamingData interrupted", ex); Thread.currentThread().interrupt(); // Restore interrupt status flag. From b2693a993b32fab2239bc66e2374662ee82e5f7e Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 7 Sep 2024 16:50:37 -0400 Subject: [PATCH 34/91] fix: Use a cache again, to handle if any recently used video ids are called from some unknown use case --- .../patches/spoof/SpoofClientPatch.java | 36 +-- .../spoof/requests/StreamingDataRequest.java | 211 ++++++++++++++++++ .../requests/StreamingDataRequester.java | 124 ---------- 3 files changed, 223 insertions(+), 148 deletions(-) create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 74532f0451..1f6a43837b 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -9,13 +9,9 @@ import java.nio.ByteBuffer; import java.util.Map; import java.util.Objects; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequester; +import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequest; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") @@ -28,8 +24,6 @@ public class SpoofClientPatch { private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0"; private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING); - private static volatile Future currentVideoStream; - /** * Injection point. * Blocks /get_watch requests by returning an unreachable URI. @@ -97,7 +91,7 @@ public static UrlRequest buildRequest(UrlRequest.Builder builder, String url, String path = uri.getPath(); if (path != null && path.contains("player") && !path.contains("heartbeat")) { String videoId = Objects.requireNonNull(uri.getQueryParameter("id")); - currentVideoStream = StreamingDataRequester.fetch(videoId, playerHeaders); + StreamingDataRequest.fetchRequestIfNeeded(videoId, playerHeaders); } } catch (Exception ex) { Logger.printException(() -> "buildRequest failure", ex); @@ -117,27 +111,21 @@ public static ByteBuffer getStreamingData(String videoId) { if (SPOOF_CLIENT) { try { // This hook is always called off the main thread, - // but can be later called for the same video id on the main thread. - // This is not a concern, since the main thread call will always be - // finished and never block. - - var future = currentVideoStream; - if (future != null) { - final long maxSecondsToWait = 20; - var stream = future.get(maxSecondsToWait, TimeUnit.SECONDS); + // but this can later be called for the same video id from the main thread. + // This is not a concern, since the fetch will always be finished + // and never block the main thread. + + StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId); + if (request != null) { + var stream = request.getStream(); if (stream != null) { Logger.printDebug(() -> "Overriding video stream: " + videoId); return stream; } - - Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId); } - } catch (TimeoutException ex) { - Logger.printInfo(() -> "getStreamingData timed out: " + videoId, ex); - } catch (InterruptedException ex) { - Logger.printException(() -> "getStreamingData interrupted", ex); - Thread.currentThread().interrupt(); // Restore interrupt status flag. - } catch (ExecutionException ex) { + + Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId); + } catch (Exception ex) { Logger.printException(() -> "getStreamingData failure", ex); } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java new file mode 100644 index 0000000000..b99e3e4fce --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java @@ -0,0 +1,211 @@ +package app.revanced.integrations.youtube.patches.spoof.requests; + +import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA; + +import android.os.Build; + +import androidx.annotation.GuardedBy; +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.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.shared.settings.BaseSettings; +import app.revanced.integrations.youtube.patches.spoof.ClientType; + +public class StreamingDataRequest { + + /** + * How long to keep fetches until they are expired. + */ + private static final long CACHE_RETENTION_TIME_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes + + private static final long MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; // 20 seconds + + @GuardedBy("itself") + private static final Map cache = new HashMap<>(); + + public static void fetchRequestIfNeeded(@Nullable String videoId, Map fetchHeaders) { + Objects.requireNonNull(videoId); + synchronized (cache) { + // Remove any expired entries. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + final long now = System.currentTimeMillis(); + cache.values().removeIf(request -> { + final boolean expired = request.isExpired(now); + if (expired) Logger.printDebug(() -> "Removing expired stream: " + request.videoId); + return expired; + }); + } + + if (!cache.containsKey(videoId)) { + cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); + } + } + } + + @Nullable + public static StreamingDataRequest getRequestForVideoId(@Nullable String videoId) { + synchronized (cache) { + return cache.get(videoId); + } + } + + private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) { + if (showToast) Utils.showToastShort(toastMessage); + Logger.printInfo(() -> toastMessage, ex); + } + + @Nullable + private static HttpURLConnection send(ClientType clientType, String videoId, + Map playerHeaders, + boolean showErrorToasts) { + Objects.requireNonNull(clientType); + Objects.requireNonNull(videoId); + Objects.requireNonNull(playerHeaders); + + final long startTime = System.currentTimeMillis(); + String clientTypeName = clientType.name(); + Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name()); + + try { + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); + + String authHeader = playerHeaders.get("Authorization"); + String visitorId = playerHeaders.get("X-Goog-Visitor-Id"); + connection.setRequestProperty("Authorization", authHeader); + connection.setRequestProperty("X-Goog-Visitor-Id", visitorId); + + String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); + byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(requestBody.length); + connection.getOutputStream().write(requestBody); + + final int responseCode = connection.getResponseCode(); + if (responseCode == 200) return connection; + + handleConnectionError(clientTypeName + " not available with response code: " + + responseCode + " message: " + connection.getResponseMessage(), + null, showErrorToasts); + } catch (SocketTimeoutException ex) { + handleConnectionError("Connection timeout", ex, showErrorToasts); + } catch (IOException ex) { + handleConnectionError("Network error", ex, showErrorToasts); + } catch (Exception ex) { + Logger.printException(() -> "send failed", ex); + } finally { + Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms"); + } + + return null; + } + + private static ByteBuffer fetch(@NonNull String videoId, Map playerHeaders) { + // Retry with different client if empty response body is received. + ClientType[] clientTypesToUse = { + ClientType.IOS, + ClientType.ANDROID_VR + }; + + final boolean debugEnabled = BaseSettings.DEBUG.get(); + if (debugEnabled) { + // To ensure the different clients are used while debugging, + // use a random client order. + Collections.shuffle(Arrays.asList(clientTypesToUse)); + } + + int i = 0; + for (ClientType clientType : clientTypesToUse) { + // Show an error if the last client type fails, or if the debug is enabled then show for all attempts. + final boolean showErrorToast = (++i == clientTypesToUse.length) || debugEnabled; + + 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) { + try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) >= 0) { + baos.write(buffer, 0, bytesRead); + } + + return ByteBuffer.wrap(baos.toByteArray()); + } + } + } + } catch (IOException ex) { + Logger.printException(() -> "Fetch failed while processing response data", ex); + } + } + } + + handleConnectionError("Could not fetch any client streams", null, debugEnabled); + return null; + } + + + /** + * Time this instance and the fetch future was created. + */ + private final long timeFetched; + private final String videoId; + private final Future future; + + private StreamingDataRequest(String videoId, Map playerHeaders) { + Objects.requireNonNull(playerHeaders); + this.timeFetched = System.currentTimeMillis(); + this.videoId = videoId; + this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders)); + } + + public boolean isExpired(long now) { + final long timeSinceCreation = now - timeFetched; + if (timeSinceCreation > CACHE_RETENTION_TIME_MILLISECONDS) { + return true; + } + + // Only expired if the fetch failed (API null response). + return (fetchCompleted() && getStream() == null); + } + + /** + * @return if the RYD fetch call has completed. + */ + public boolean fetchCompleted() { + return future.isDone(); + } + + @Nullable + public ByteBuffer getStream() { + try { + return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS); + } catch (TimeoutException ex) { + Logger.printInfo(() -> "getStream timed out", ex); + } catch (InterruptedException ex) { + Logger.printException(() -> "getStream interrupted", ex); + Thread.currentThread().interrupt(); // Restore interrupt status flag. + } catch (ExecutionException ex) { + Logger.printException(() -> "getStream failure", ex); + } + + return null; + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java deleted file mode 100644 index 113fd264f8..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java +++ /dev/null @@ -1,124 +0,0 @@ -package app.revanced.integrations.youtube.patches.spoof.requests; - -import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.GET_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.*; -import java.util.concurrent.Future; - -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; -import app.revanced.integrations.shared.settings.BaseSettings; -import app.revanced.integrations.youtube.patches.spoof.ClientType; - -public class StreamingDataRequester { - - private StreamingDataRequester() { - } - - private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) { - if (showToast) Utils.showToastShort(toastMessage); - Logger.printInfo(() -> toastMessage, ex); - } - - @Nullable - private static HttpURLConnection send(ClientType clientType, String videoId, - Map playerHeaders, - boolean showErrorToasts) { - final long startTime = System.currentTimeMillis(); - String clientTypeName = clientType.name(); - Logger.printDebug(() -> "Fetching video streams using client: " + clientType.name()); - - try { - HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); - - String authHeader = playerHeaders.get("Authorization"); - String visitorId = playerHeaders.get("X-Goog-Visitor-Id"); - connection.setRequestProperty("Authorization", authHeader); - connection.setRequestProperty("X-Goog-Visitor-Id", visitorId); - - String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); - byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); - connection.setFixedLengthStreamingMode(requestBody.length); - connection.getOutputStream().write(requestBody); - - final int responseCode = connection.getResponseCode(); - if (responseCode == 200) return connection; - - handleConnectionError(clientTypeName + " not available with response code: " - + responseCode + " message: " + connection.getResponseMessage(), - null, showErrorToasts); - } catch (SocketTimeoutException ex) { - handleConnectionError("Connection timeout", ex, showErrorToasts); - } catch (IOException ex) { - handleConnectionError("Network error", ex, showErrorToasts); - } catch (Exception ex) { - Logger.printException(() -> "send failed", ex); - } finally { - Logger.printDebug(() -> clientTypeName + " took: " + (System.currentTimeMillis() - startTime) + "ms"); - } - - return null; - } - - public static Future fetch(@NonNull String videoId, Map playerHeaders) { - Objects.requireNonNull(videoId); - - return Utils.submitOnBackgroundThread(() -> { - // Retry with different client if empty response body is received. - ClientType[] clientTypesToUse = { - ClientType.IOS, - ClientType.ANDROID_VR - }; - - final boolean debugEnabled = BaseSettings.DEBUG.get(); - if (debugEnabled) { - // To ensure the different clients are used while debugging, - // use a random client order. - Collections.shuffle(Arrays.asList(clientTypesToUse)); - } - - int i = 0; - for (ClientType clientType : clientTypesToUse) { - // Show an error if the last client type fails, or if the debug is enabled then show for all attempts. - final boolean showErrorToast = (++i == clientTypesToUse.length) || debugEnabled; - - 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) { - try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - byte[] buffer = new byte[8192]; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) >= 0) { - baos.write(buffer, 0, bytesRead); - } - - return ByteBuffer.wrap(baos.toByteArray()); - } - } - } - } catch (IOException ex) { - Logger.printException(() -> "Fetch failed while processing response data", ex); - } - } - } - - handleConnectionError("Could not fetch any client streams", null, debugEnabled); - return null; - }); - } -} From 0bd6dc543bec400f9668f03af2b86a6b11ec1f44 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 7 Sep 2024 17:29:00 -0400 Subject: [PATCH 35/91] fix: Always fetch if YT is fetching --- .../youtube/patches/spoof/SpoofClientPatch.java | 2 +- .../patches/spoof/requests/StreamingDataRequest.java | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 1f6a43837b..ffa99760ee 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -91,7 +91,7 @@ public static UrlRequest buildRequest(UrlRequest.Builder builder, String url, String path = uri.getPath(); if (path != null && path.contains("player") && !path.contains("heartbeat")) { String videoId = Objects.requireNonNull(uri.getQueryParameter("id")); - StreamingDataRequest.fetchRequestIfNeeded(videoId, playerHeaders); + StreamingDataRequest.fetchRequest(videoId, playerHeaders); } } catch (Exception ex) { Logger.printException(() -> "buildRequest failure", ex); diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java index b99e3e4fce..f80047eb70 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java @@ -32,14 +32,14 @@ public class StreamingDataRequest { /** * How long to keep fetches until they are expired. */ - private static final long CACHE_RETENTION_TIME_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes + private static final long CACHE_RETENTION_TIME_MILLISECONDS = 60 * 60 * 1000; // 1 hour private static final long MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; // 20 seconds @GuardedBy("itself") private static final Map cache = new HashMap<>(); - public static void fetchRequestIfNeeded(@Nullable String videoId, Map fetchHeaders) { + public static void fetchRequest(@Nullable String videoId, Map fetchHeaders) { Objects.requireNonNull(videoId); synchronized (cache) { // Remove any expired entries. @@ -52,9 +52,8 @@ public static void fetchRequestIfNeeded(@Nullable String videoId, Map Date: Sat, 7 Sep 2024 17:39:10 -0400 Subject: [PATCH 36/91] fix: Don't use an cache expiration for streams since they are always re-fetched, and instead cache a fixed number of recent fetches --- .../spoof/requests/StreamingDataRequest.java | 73 +++++-------------- 1 file changed, 20 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java index f80047eb70..5cdfe56988 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java @@ -2,9 +2,6 @@ import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA; -import android.os.Build; - -import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -29,39 +26,26 @@ public class StreamingDataRequest { - /** - * How long to keep fetches until they are expired. - */ - private static final long CACHE_RETENTION_TIME_MILLISECONDS = 60 * 60 * 1000; // 1 hour - private static final long MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; // 20 seconds - @GuardedBy("itself") - private static final Map cache = new HashMap<>(); + private static final Map cache = Collections.synchronizedMap( + new LinkedHashMap<>(100) { + private static final int CACHE_LIMIT = 50; - public static void fetchRequest(@Nullable String videoId, Map fetchHeaders) { - Objects.requireNonNull(videoId); - synchronized (cache) { - // Remove any expired entries. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - final long now = System.currentTimeMillis(); - cache.values().removeIf(request -> { - final boolean expired = request.isExpired(now); - if (expired) Logger.printDebug(() -> "Removing expired stream: " + request.videoId); - return expired; - }); - } + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit. + } + }); - // Always fetch, even if there is a existing request for the same video. - cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); - } + public static void fetchRequest(String videoId, Map fetchHeaders) { + // Always fetch, even if there is a existing request for the same video. + cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); } @Nullable - public static StreamingDataRequest getRequestForVideoId(@Nullable String videoId) { - synchronized (cache) { - return cache.get(videoId); - } + public static StreamingDataRequest getRequestForVideoId(String videoId) { + return cache.get(videoId); } private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) { @@ -113,7 +97,7 @@ private static HttpURLConnection send(ClientType clientType, String videoId, return null; } - private static ByteBuffer fetch(@NonNull String videoId, Map playerHeaders) { + private static ByteBuffer fetch(String videoId, Map playerHeaders) { // Retry with different client if empty response body is received. ClientType[] clientTypesToUse = { ClientType.IOS, @@ -160,38 +144,15 @@ private static ByteBuffer fetch(@NonNull String videoId, Map pla return null; } - - /** - * Time this instance and the fetch future was created. - */ - private final long timeFetched; private final String videoId; private final Future future; private StreamingDataRequest(String videoId, Map playerHeaders) { Objects.requireNonNull(playerHeaders); - this.timeFetched = System.currentTimeMillis(); this.videoId = videoId; this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders)); } - public boolean isExpired(long now) { - final long timeSinceCreation = now - timeFetched; - if (timeSinceCreation > CACHE_RETENTION_TIME_MILLISECONDS) { - return true; - } - - // Only expired if the fetch failed (API null response). - return (fetchCompleted() && getStream() == null); - } - - /** - * @return if the RYD fetch call has completed. - */ - public boolean fetchCompleted() { - return future.isDone(); - } - @Nullable public ByteBuffer getStream() { try { @@ -207,4 +168,10 @@ public ByteBuffer getStream() { return null; } + + @NonNull + @Override + public String toString() { + return "StreamingDataRequest{" + "videoId='" + videoId + '\'' + '}'; + } } From b55a979a3d502b0f5d32e68b62afe0629dbc6af2 Mon Sep 17 00:00:00 2001 From: zainarbani Date: Sat, 7 Sep 2024 20:28:55 +0700 Subject: [PATCH 37/91] feat(YouTube): Support version `~19.31` --- .../patches/components/LithoFilterPatch.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java index a7c9972004..80a7d3b44f 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java @@ -456,6 +456,8 @@ private static void findAsciiStrings(StringBuilder builder, byte[] buffer) { private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + private static volatile boolean isFiltered; + /** * Because litho filtering is multi-threaded and the buffer is passed in from a different injection point, * the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads. @@ -518,11 +520,13 @@ public static void setProtoBuffer(@Nullable ByteBuffer protobufBuffer) { * Injection point. Called off the main thread, and commonly called by multiple threads at the same time. */ @SuppressWarnings("unused") - public static boolean filter(@Nullable String lithoIdentifier, @NonNull StringBuilder pathBuilder) { + public static void filter(@Nullable String lithoIdentifier, @NonNull StringBuilder pathBuilder) { + // Always clear filter state. + isFiltered = false; try { // It is assumed that protobufBuffer is empty as well in this case. if (pathBuilder.length() == 0) - return false; + return; ByteBuffer protobufBuffer = bufferThreadLocal.get(); final byte[] bufferArray; @@ -543,13 +547,18 @@ public static boolean filter(@Nullable String lithoIdentifier, @NonNull StringBu Logger.printDebug(() -> "Searching " + parameter); if (parameter.identifier != null) { - if (identifierSearchTree.matches(parameter.identifier, parameter)) return true; + isFiltered = identifierSearchTree.matches(parameter.identifier, parameter); + return; } - if (pathSearchTree.matches(parameter.path, parameter)) return true; + isFiltered = pathSearchTree.matches(parameter.path, parameter); + return; } catch (Exception ex) { Logger.printException(() -> "Litho filter failure", ex); } + } - return false; + @SuppressWarnings("unused") + public static boolean filterState() { + return isFiltered; } } \ No newline at end of file From 0cc000d4c59ff73b8d1223e5244280465233b791 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 7 Sep 2024 20:01:46 -0400 Subject: [PATCH 38/91] refactor: Use a single enum for all library tabs --- .../components/LayoutComponentsFilter.java | 3 +- .../youtube/shared/NavigationBar.java | 66 ++++++++----------- 2 files changed, 29 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java index a0ffde0b8c..55cfd3f63b 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java @@ -406,7 +406,6 @@ private static boolean hideShelves() { // Check navigation button last. // Only filter if the library tab is not selected. // This check is important as the shelf layout is used for the library tab playlists. - NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); - return selectedNavButton != null && !selectedNavButton.isLibraryOrYouTab(); + return NavigationButton.getSelectedNavigationButton() != NavigationButton.LIBRARY; } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java b/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java index 5123e78d6a..7a45b173b4 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java +++ b/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java @@ -8,6 +8,8 @@ import androidx.annotation.Nullable; import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.CountDownLatch; @@ -156,11 +158,11 @@ public static void navigationTabLoaded(final View navigationButtonGroup) { try { String lastEnumName = lastYTNavigationEnumName; - for (NavigationButton button : NavigationButton.values()) { - if (button.ytEnumName.equals(lastEnumName)) { + for (NavigationButton buttonType : NavigationButton.values()) { + if (buttonType.ytEnumNames.contains(lastEnumName)) { Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName); - viewToButtonMap.put(navigationButtonGroup, button); - navigationTabCreatedCallback(button, navigationButtonGroup); + viewToButtonMap.put(navigationButtonGroup, buttonType); + navigationTabCreatedCallback(buttonType, navigationButtonGroup); return; } } @@ -184,10 +186,10 @@ public static void navigationTabLoaded(final View navigationButtonGroup) { public static void navigationImageResourceTabLoaded(View view) { // 'You' tab has no YT enum name and the enum hook is not called for it. // Compare the last enum to figure out which tab this actually is. - if (CREATE.ytEnumName.equals(lastYTNavigationEnumName)) { + if (CREATE.ytEnumNames.contains(lastYTNavigationEnumName)) { navigationTabLoaded(view); } else { - lastYTNavigationEnumName = NavigationButton.LIBRARY_YOU.ytEnumName; + lastYTNavigationEnumName = NavigationButton.LIBRARY.ytEnumNames.get(0); navigationTabLoaded(view); } } @@ -251,30 +253,24 @@ public enum NavigationButton { */ NOTIFICATIONS("TAB_ACTIVITY"), /** - * Library tab when the user is not logged in. + * Library tab, including if the user is in incognito mode or when logged out. */ - LIBRARY_LOGGED_OUT("ACCOUNT_CIRCLE"), - /** - * User is logged in with incognito mode enabled. - */ - LIBRARY_INCOGNITO("INCOGNITO_CIRCLE"), - /** - * Old library tab (pre 'You' layout), only present when version spoofing. - */ - LIBRARY_OLD_UI("VIDEO_LIBRARY_WHITE"), - /** - * 'You' library tab that is sometimes momentarily loaded. - * When this is loaded, {@link #LIBRARY_YOU} is also present. - * - * This might be a temporary tab while the user profile photo is loading, - * but its exact purpose is not entirely clear. - */ - LIBRARY_PIVOT_UNKNOWN("PIVOT_LIBRARY"), - /** - * Modern library tab with 'You' layout. - */ - // The hooked YT code does not use an enum, and a dummy name is used here. - LIBRARY_YOU("YOU_LIBRARY_DUMMY_PLACEHOLDER_NAME"); + LIBRARY( + // Modern library tab with 'You' layout. + // The hooked YT code does not use an enum, and a dummy name is used here. + "YOU_LIBRARY_DUMMY_PLACEHOLDER_NAME", + // User is logged out. + "ACCOUNT_CIRCLE", + // User is logged in with incognito mode enabled. + "INCOGNITO_CIRCLE", + // Old library tab (pre 'You' layout), only present when version spoofing. + "VIDEO_LIBRARY_WHITE", + // 'You' library tab that is sometimes momentarily loaded. + // When this is loaded, {@link #LIBRARY_YOU} is also present. + // This might be a temporary tab while the user profile photo is loading, + // but its exact purpose is not entirely clear. + "PIVOT_LIBRARY" + ); @Nullable private static volatile NavigationButton selectedNavigationButton; @@ -303,16 +299,10 @@ public static NavigationButton getSelectedNavigationButton() { /** * YouTube enum name for this tab. */ - private final String ytEnumName; - - NavigationButton(String ytEnumName) { - this.ytEnumName = ytEnumName; - } + private final List ytEnumNames; - public boolean isLibraryOrYouTab() { - return this == LIBRARY_YOU || this == LIBRARY_PIVOT_UNKNOWN - || this == LIBRARY_OLD_UI || this == LIBRARY_INCOGNITO - || this == LIBRARY_LOGGED_OUT; + NavigationButton(String... ytEnumNames) { + this.ytEnumNames = Arrays.asList(ytEnumNames); } } } From 92e17bf940765d6de7bb7e9419ed06b00f4761c6 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 7 Sep 2024 20:09:28 -0400 Subject: [PATCH 39/91] fix: Add cairo style navigation tabs --- .../integrations/youtube/shared/NavigationBar.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java b/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java index 7a45b173b4..4c220d09e2 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java +++ b/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java @@ -239,19 +239,19 @@ private static void navigationTabCreatedCallback(NavigationButton button, View t } public enum NavigationButton { - HOME("PIVOT_HOME"), - SHORTS("TAB_SHORTS"), + HOME("PIVOT_HOME", "TAB_HOME_CAIRO"), + SHORTS("TAB_SHORTS", "TAB_SHORTS_CAIRO"), /** * Create new video tab. * This tab will never be in a selected state, even if the create video UI is on screen. */ - CREATE("CREATION_TAB_LARGE"), - SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS"), + CREATE("CREATION_TAB_LARGE", "CREATION_TAB_LARGE_CAIRO"), + SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS", "TAB_SUBSCRIPTIONS_CAIRO"), /** * Notifications tab. Only present when * {@link Settings#SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON} is active. */ - NOTIFICATIONS("TAB_ACTIVITY"), + NOTIFICATIONS("TAB_ACTIVITY", "TAB_ACTIVITY_CAIRO"), /** * Library tab, including if the user is in incognito mode or when logged out. */ @@ -261,12 +261,13 @@ public enum NavigationButton { "YOU_LIBRARY_DUMMY_PLACEHOLDER_NAME", // User is logged out. "ACCOUNT_CIRCLE", + "ACCOUNT_CIRCLE_CAIRO", // User is logged in with incognito mode enabled. "INCOGNITO_CIRCLE", + "INCOGNITO_CAIRO", // Old library tab (pre 'You' layout), only present when version spoofing. "VIDEO_LIBRARY_WHITE", // 'You' library tab that is sometimes momentarily loaded. - // When this is loaded, {@link #LIBRARY_YOU} is also present. // This might be a temporary tab while the user profile photo is loading, // but its exact purpose is not entirely clear. "PIVOT_LIBRARY" From 01de4f5d0ca17f7e476dd21599c593e5996d50e7 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 7 Sep 2024 21:54:47 -0400 Subject: [PATCH 40/91] fix: Use thread local to fix multi-threaded clashing --- .../patches/components/LithoFilterPatch.java | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java index 80a7d3b44f..dc65eb3b06 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java @@ -456,13 +456,12 @@ private static void findAsciiStrings(StringBuilder builder, byte[] buffer) { private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; - private static volatile boolean isFiltered; - /** * Because litho filtering is multi-threaded and the buffer is passed in from a different injection point, * the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads. */ private static final ThreadLocal bufferThreadLocal = new ThreadLocal<>(); + private static final ThreadLocal filteredState = new ThreadLocal<>(); static { for (Filter filter : filters) { @@ -516,17 +515,29 @@ public static void setProtoBuffer(@Nullable ByteBuffer protobufBuffer) { } } + private static void setFilteredState(boolean filtered) { + filteredState.set(filtered); + } + + /** + * Injection point. + */ + @SuppressWarnings("unused") + public static boolean filterState() { + Boolean state = filteredState.get(); + return state != null && state; + } + /** * Injection point. Called off the main thread, and commonly called by multiple threads at the same time. */ @SuppressWarnings("unused") public static void filter(@Nullable String lithoIdentifier, @NonNull StringBuilder pathBuilder) { - // Always clear filter state. - isFiltered = false; try { - // It is assumed that protobufBuffer is empty as well in this case. - if (pathBuilder.length() == 0) + if (pathBuilder.length() == 0) { + setFilteredState(false); return; + } ByteBuffer protobufBuffer = bufferThreadLocal.get(); final byte[] bufferArray; @@ -546,19 +557,19 @@ public static void filter(@Nullable String lithoIdentifier, @NonNull StringBuild pathBuilder.toString(), bufferArray); Logger.printDebug(() -> "Searching " + parameter); - if (parameter.identifier != null) { - isFiltered = identifierSearchTree.matches(parameter.identifier, parameter); + if (parameter.identifier != null && identifierSearchTree.matches(parameter.identifier, parameter)) { + setFilteredState(true); + return; + } + + if (pathSearchTree.matches(parameter.path, parameter)) { + setFilteredState(true); return; } - isFiltered = pathSearchTree.matches(parameter.path, parameter); - return; } catch (Exception ex) { Logger.printException(() -> "Litho filter failure", ex); } - } - @SuppressWarnings("unused") - public static boolean filterState() { - return isFiltered; + setFilteredState(false); } } \ No newline at end of file From c2c77d0e58b8db761651d8e6998d828cbf6fd59b Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 8 Sep 2024 02:11:15 -0400 Subject: [PATCH 41/91] fix: Fix crashing on newer versions. Do not hide skip ad button in miniplayer (if ads are not hidden) --- .../youtube/patches/MiniplayerPatch.java | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java index 22ea9f1017..dd0ad856e6 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java @@ -6,7 +6,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.Nullable; @@ -140,27 +139,37 @@ public static void hideMiniplayerRewindForward(ImageView view) { * Injection point. */ public static void hideMiniplayerSubTexts(View view) { - // Different subviews are passed in, but only TextView and layouts are of interest here. - final boolean hideView = HIDE_SUBTEXT_ENABLED && (view instanceof TextView || view instanceof LinearLayout); - Utils.hideViewByRemovingFromParentUnderCondition(hideView, view); + try { + // Different subviews are passed in, but only TextView is of interest here. + if (HIDE_SUBTEXT_ENABLED && view instanceof TextView) { + Logger.printDebug(() -> "Hiding subtext view"); + Utils.hideViewByRemovingFromParentUnderCondition(true, view); + } + } catch (Exception ex) { + Logger.printException(() -> "hideMiniplayerSubTexts failure", ex); + } } /** * Injection point. */ public static void playerOverlayGroupCreated(View group) { - // Modern 2 has an half broken subtitle that is always present. - // Always hide it to make the miniplayer mostly usable. - if (CURRENT_TYPE == MODERN_2 && MODERN_OVERLAY_SUBTITLE_TEXT != 0) { - if (group instanceof ViewGroup) { - View subtitleText = Utils.getChildView((ViewGroup) group, true, - view -> view.getId() == MODERN_OVERLAY_SUBTITLE_TEXT); - - if (subtitleText != null) { - subtitleText.setVisibility(View.GONE); - Logger.printDebug(() -> "Modern overlay subtitle view set to hidden"); + try { + // Modern 2 has an half broken subtitle that is always present. + // Always hide it to make the miniplayer mostly usable. + if (CURRENT_TYPE == MODERN_2 && MODERN_OVERLAY_SUBTITLE_TEXT != 0) { + if (group instanceof ViewGroup) { + View subtitleText = Utils.getChildView((ViewGroup) group, true, + view -> view.getId() == MODERN_OVERLAY_SUBTITLE_TEXT); + + if (subtitleText != null) { + subtitleText.setVisibility(View.GONE); + Logger.printDebug(() -> "Modern overlay subtitle view set to hidden"); + } } } + } catch (Exception ex) { + Logger.printException(() -> "playerOverlayGroupCreated failure", ex); } } } From 60bc6f1790287d23d0ba9ad0220ae37109e608b0 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 8 Sep 2024 02:49:00 -0400 Subject: [PATCH 42/91] fix(Background play): Fix Premium user unusual edge case failure --- .../integrations/youtube/patches/BackgroundPlaybackPatch.java | 4 +++- .../integrations/youtube/patches/spoof/SpoofClientPatch.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/BackgroundPlaybackPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/BackgroundPlaybackPatch.java index e6e449a0cb..d84813d014 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/BackgroundPlaybackPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/BackgroundPlaybackPatch.java @@ -8,7 +8,9 @@ public class BackgroundPlaybackPatch { /** * Injection point. */ - public static boolean playbackIsNotShort() { + public static boolean allowBackgroundPlayback(boolean original) { + if (original) return true; + // Steps to verify most edge cases: // 1. Open a regular video // 2. Minimize app (PIP should appear) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 14e5e2f1b4..2b4f04d863 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -134,7 +134,7 @@ public static boolean forceCreatePlaybackSpeedMenu(boolean original) { * Return true to force enable audio background play. */ public static boolean overrideBackgroundAudioPlayback() { - return SPOOF_IOS && BackgroundPlaybackPatch.playbackIsNotShort(); + return SPOOF_IOS && BackgroundPlaybackPatch.allowBackgroundPlayback(false); } /** From a9bd260257362fde721b44494d9b841204bb998c Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 8 Sep 2024 07:42:06 -0400 Subject: [PATCH 43/91] fix(Miniplayer): Add Modern 1 drag and drop, double tap action --- .../youtube/patches/MiniplayerPatch.java | 34 ++++++++++++++++++- .../youtube/settings/Settings.java | 8 +++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java index dd0ad856e6..9750d9a258 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java @@ -12,6 +12,7 @@ import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.shared.settings.Setting; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") @@ -60,8 +61,16 @@ public boolean isModern() { private static final MiniplayerType CURRENT_TYPE = Settings.MINIPLAYER_TYPE.get(); + private static final boolean DOUBLE_TAP_ACTION_ENABLED = + (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_2 || CURRENT_TYPE == MODERN_3) + && Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get(); + + private static final boolean DRAG_AND_DROP_ENABLED = + CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_DRAG_AND_DROP.get(); + private static final boolean HIDE_EXPAND_CLOSE_ENABLED = - (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get(); + (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) + && !DRAG_AND_DROP_ENABLED && Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get(); private static final boolean HIDE_SUBTEXT_ENABLED = (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_SUBTEXT.get(); @@ -71,6 +80,15 @@ public boolean isModern() { private static final int OPACITY_LEVEL; + public static final class MiniplayerHideExpandCloseAvailability implements Setting.Availability { + Setting.Availability modernOneOrThree = Settings.MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3); + + @Override + public boolean isAvailable() { + return modernOneOrThree.isAvailable() && !Settings.MINIPLAYER_DRAG_AND_DROP.get(); + } + } + static { int opacity = Settings.MINIPLAYER_OPACITY.get(); @@ -121,6 +139,20 @@ public static void adjustMiniplayerOpacity(ImageView view) { } } + /** + * Injection point. + */ + public static boolean enableMiniplayerDoubleTapAction(boolean original) { + return DOUBLE_TAP_ACTION_ENABLED; + } + + /** + * Injection point. + */ + public static boolean enableMiniplayerDragAndDrop(boolean original) { + return DRAG_AND_DROP_ENABLED; + } + /** * Injection point. */ diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 5a40ed0fe8..f623ec4498 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -16,9 +16,9 @@ import java.util.Set; import static app.revanced.integrations.shared.settings.Setting.*; +import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerHideExpandCloseAvailability; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType; -import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1; -import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3; +import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*; import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*; import static java.lang.Boolean.FALSE; @@ -136,7 +136,9 @@ public class Settings extends BaseSettings { // Miniplayer public static final EnumSetting MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.ORIGINAL, true); - public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3)); + public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_enable_double_tap_action", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3)); + public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_enable_drag_and_drop", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1)); + public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true, new MiniplayerHideExpandCloseAvailability()); public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3)); public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1)); public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, MINIPLAYER_TYPE.availability(MODERN_1)); From 90f84d6904ea4d5edb05814b8c034059cbee02da Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 8 Sep 2024 10:13:46 -0400 Subject: [PATCH 44/91] feat(Miniplayer): Customize `19.28` miniplayer size --- .../youtube/patches/MiniplayerPatch.java | 42 +++++++++++++++++++ .../youtube/settings/Settings.java | 1 + 2 files changed, 43 insertions(+) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java index 9750d9a258..2667cf2f4b 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java @@ -3,6 +3,7 @@ import static app.revanced.integrations.shared.StringRef.str; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*; +import android.util.DisplayMetrics; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; @@ -52,6 +53,36 @@ public boolean isModern() { } } + private static final int MINIPLAYER_SIZE; + + static { + // YT appears to use the device screen dip width, plus an unknown fixed horizontal padding size. + DisplayMetrics displayMetrics = Utils.getContext().getResources().getDisplayMetrics(); + final int deviceDipWidth = (int) (displayMetrics.widthPixels / displayMetrics.density); + + // YT seems to use a minimum height to calculate + // the minimum miniplayer width based on the video. + // 170 seems to be the smallest that can be used and using less makes no difference. + final int WIDTH_DIP_MIN = 170; // Seems to be the smallest that works. + final int HORIZONTAL_PADDING_DIP = 15; // Estimated padding. + // Round down to the nearest 5 pixels, to keep any error toasts easier to read. + final int WIDTH_DIP_MAX = 5 * ((deviceDipWidth - HORIZONTAL_PADDING_DIP) / 5); + Logger.printDebug(() -> "Screen dip width: " + deviceDipWidth + " maxWidth: " + WIDTH_DIP_MAX); + + int dipWidth = Settings.MINIPLAYER_WIDTH_DIP.get(); + + if (dipWidth < WIDTH_DIP_MIN || dipWidth > WIDTH_DIP_MAX) { + Utils.showToastLong(str("revanced_miniplayer_width_dip_invalid_toast", + WIDTH_DIP_MIN, WIDTH_DIP_MAX)); + + // Instead of resetting, clamp the size at the bounds. + dipWidth = Math.max(WIDTH_DIP_MIN, Math.min(dipWidth, WIDTH_DIP_MAX)); + Settings.MINIPLAYER_WIDTH_DIP.save(dipWidth); + } + + MINIPLAYER_SIZE = dipWidth; + } + /** * Modern subtitle overlay for {@link MiniplayerType#MODERN_2}. * Resource is not present in older targets, and this field will be zero. @@ -153,6 +184,17 @@ public static boolean enableMiniplayerDragAndDrop(boolean original) { return DRAG_AND_DROP_ENABLED; } + /** + * Injection point. + */ + public static int setMiniplayerSize(int original) { + if (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_2 || CURRENT_TYPE == MODERN_3) { + return MINIPLAYER_SIZE; + } + + return original; + } + /** * Injection point. */ diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index f623ec4498..011e2cc2a7 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -136,6 +136,7 @@ public class Settings extends BaseSettings { // Miniplayer public static final EnumSetting MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.ORIGINAL, true); + public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3)); public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_enable_double_tap_action", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3)); public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_enable_drag_and_drop", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1)); public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true, new MiniplayerHideExpandCloseAvailability()); From d6782c55d4dfb7f628a5a3c8973c98ea92f3145b Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 8 Sep 2024 23:10:29 -0400 Subject: [PATCH 45/91] fix Sponsorblock overlay buttons --- .../app/revanced/integrations/shared/Utils.java | 14 ++++++++++++++ .../ui/CreateSegmentButtonController.java | 10 +++------- .../sponsorblock/ui/VotingButtonController.java | 10 +++------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/shared/Utils.java b/app/src/main/java/app/revanced/integrations/shared/Utils.java index 22ed1e0624..0284447b9e 100644 --- a/app/src/main/java/app/revanced/integrations/shared/Utils.java +++ b/app/src/main/java/app/revanced/integrations/shared/Utils.java @@ -268,6 +268,20 @@ public interface MatchFilter { boolean matches(T object); } + /** + * Includes sub children. + * + * @noinspection unchecked + */ + public static R getChildViewByResourceName(@NonNull View view, @NonNull String str) { + var child = view.findViewById(Utils.getResourceIdentifier(str, "id")); + if (child != null) { + return (R) child; + } + + throw new IllegalArgumentException("View with resource name '" + str + "' not found"); + } + /** * @param searchRecursively If children ViewGroups should also be * recursively searched using depth first search. diff --git a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/CreateSegmentButtonController.java b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/CreateSegmentButtonController.java index dffdf9ff48..c13d696b29 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/CreateSegmentButtonController.java +++ b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/CreateSegmentButtonController.java @@ -24,8 +24,8 @@ public class CreateSegmentButtonController { public static void initialize(View youtubeControlsLayout) { try { Logger.printDebug(() -> "initializing new segment button"); - ImageView imageView = Objects.requireNonNull(youtubeControlsLayout.findViewById( - getResourceIdentifier("revanced_sb_create_segment_button", "id"))); + ImageView imageView = Objects.requireNonNull(Utils.getChildViewByResourceName( + youtubeControlsLayout, "revanced_sb_create_segment_button")); imageView.setVisibility(View.GONE); imageView.setOnClickListener(v -> { SponsorBlockViewController.toggleNewSegmentLayoutVisibility(); @@ -37,10 +37,6 @@ public static void initialize(View youtubeControlsLayout) { } } - public static void changeVisibilityImmediate(boolean visible) { - changeVisibility(visible, true); - } - /** * injection point */ @@ -55,7 +51,7 @@ public static void changeVisibility(boolean visible) { changeVisibility(visible, false); } - public static void changeVisibility(boolean visible, boolean immediate) { + private static void changeVisibility(boolean visible, boolean immediate) { try { if (isShowing == visible) return; isShowing = visible; diff --git a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/VotingButtonController.java b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/VotingButtonController.java index 581ba1ada5..9923d4bec2 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/VotingButtonController.java +++ b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/VotingButtonController.java @@ -26,8 +26,8 @@ public class VotingButtonController { public static void initialize(View youtubeControlsLayout) { try { Logger.printDebug(() -> "initializing voting button"); - ImageView imageView = Objects.requireNonNull(youtubeControlsLayout.findViewById( - getResourceIdentifier("revanced_sb_voting_button", "id"))); + ImageView imageView = Objects.requireNonNull(Utils.getChildViewByResourceName( + youtubeControlsLayout, "revanced_sb_voting_button")); imageView.setVisibility(View.GONE); imageView.setOnClickListener(v -> { SponsorBlockUtils.onVotingClicked(v.getContext()); @@ -39,10 +39,6 @@ public static void initialize(View youtubeControlsLayout) { } } - public static void changeVisibilityImmediate(boolean visible) { - changeVisibility(visible, true); - } - /** * injection point */ @@ -57,7 +53,7 @@ public static void changeVisibility(boolean visible) { changeVisibility(visible, false); } - public static void changeVisibility(boolean visible, boolean immediate) { + private static void changeVisibility(boolean visible, boolean immediate) { try { if (isShowing == visible) return; isShowing = visible; From 4d95b8de9726e184b38a8d9c8d612c9ab1ddf7e7 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:42:53 -0400 Subject: [PATCH 46/91] refactor: Move some classes to their own files --- .../youtube/patches/components/Filter.java | 90 +++++ .../patches/components/FilterGroup.java | 214 ++++++++++ .../patches/components/FilterGroupList.java | 85 ++++ .../patches/components/LithoFilterPatch.java | 381 +----------------- 4 files changed, 395 insertions(+), 375 deletions(-) create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/components/Filter.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroup.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroupList.java diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/Filter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/Filter.java new file mode 100644 index 0000000000..ed1c56c92f --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/Filter.java @@ -0,0 +1,90 @@ +package app.revanced.integrations.youtube.patches.components; + +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.settings.BaseSettings; + +/** + * Filters litho based components. + * + * Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)} + * and {@link #addPathCallbacks(StringFilterGroup...)}. + * + * To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to + * either an identifier or a path. + * Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} + * search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern) + * or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern). + * + * All callbacks must be registered before the constructor completes. + */ +abstract class Filter { + + public enum FilterContentType { + IDENTIFIER, + PATH, + PROTOBUFFER + } + + /** + * Identifier callbacks. Do not add to this instance, + * and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}. + */ + protected final List identifierCallbacks = new ArrayList<>(); + /** + * Path callbacks. Do not add to this instance, + * and instead use {@link #addPathCallbacks(StringFilterGroup...)}. + */ + protected final List pathCallbacks = new ArrayList<>(); + + /** + * Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} + * if any of the groups are found. + */ + protected final void addIdentifierCallbacks(StringFilterGroup... groups) { + identifierCallbacks.addAll(Arrays.asList(groups)); + } + + /** + * Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} + * if any of the groups are found. + */ + protected final void addPathCallbacks(StringFilterGroup... groups) { + pathCallbacks.addAll(Arrays.asList(groups)); + } + + /** + * Called after an enabled filter has been matched. + * Default implementation is to always filter the matched component and log the action. + * Subclasses can perform additional or different checks if needed. + *

+ * If the content is to be filtered, subclasses should always + * call this method (and never return a plain 'true'). + * That way the logs will always show when a component was filtered and which filter hide it. + *

+ * Method is called off the main thread. + * + * @param matchedGroup The actual filter that matched. + * @param contentType The type of content matched. + * @param contentIndex Matched index of the identifier or path. + * @return True if the litho component should be filtered out. + */ + boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (BaseSettings.DEBUG.get()) { + String filterSimpleName = getClass().getSimpleName(); + if (contentType == FilterContentType.IDENTIFIER) { + Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier); + } else { + Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path); + } + } + return true; + } +} + diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroup.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroup.java new file mode 100644 index 0000000000..5cc101593a --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroup.java @@ -0,0 +1,214 @@ +package app.revanced.integrations.youtube.patches.components; + +import androidx.annotation.NonNull; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.settings.BooleanSetting; +import app.revanced.integrations.youtube.ByteTrieSearch; + +abstract class FilterGroup { + final static class FilterGroupResult { + private BooleanSetting setting; + private int matchedIndex; + private int matchedLength; + // In the future it might be useful to include which pattern matched, + // but for now that is not needed. + + FilterGroupResult() { + this(null, -1, 0); + } + + FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) { + setValues(setting, matchedIndex, matchedLength); + } + + public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) { + this.setting = setting; + this.matchedIndex = matchedIndex; + this.matchedLength = matchedLength; + } + + /** + * A null value if the group has no setting, + * or if no match is returned from {@link FilterGroupList#check(Object)}. + */ + public BooleanSetting getSetting() { + return setting; + } + + public boolean isFiltered() { + return matchedIndex >= 0; + } + + /** + * Matched index of first pattern that matched, or -1 if nothing matched. + */ + public int getMatchedIndex() { + return matchedIndex; + } + + /** + * Length of the matched filter pattern. + */ + public int getMatchedLength() { + return matchedLength; + } + } + + protected final BooleanSetting setting; + protected final T[] filters; + + /** + * Initialize a new filter group. + * + * @param setting The associated setting. + * @param filters The filters. + */ + @SafeVarargs + public FilterGroup(final BooleanSetting setting, final T... filters) { + this.setting = setting; + this.filters = filters; + if (filters.length == 0) { + throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)"); + } + } + + public boolean isEnabled() { + return setting == null || setting.get(); + } + + /** + * @return If {@link FilterGroupList} should include this group when searching. + * By default, all filters are included except non enabled settings that require reboot. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean includeInSearch() { + return isEnabled() || !setting.rebootApp; + } + + @NonNull + @Override + public String toString() { + return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting); + } + + public abstract FilterGroupResult check(final T stack); +} + +class StringFilterGroup extends FilterGroup { + + public StringFilterGroup(final BooleanSetting setting, final String... filters) { + super(setting, filters); + } + + @Override + public FilterGroupResult check(final String string) { + int matchedIndex = -1; + int matchedLength = 0; + if (isEnabled()) { + for (String pattern : filters) { + if (!string.isEmpty()) { + final int indexOf = string.indexOf(pattern); + if (indexOf >= 0) { + matchedIndex = indexOf; + matchedLength = pattern.length(); + break; + } + } + } + } + return new FilterGroupResult(setting, matchedIndex, matchedLength); + } +} + +/** + * If you have more than 1 filter patterns, then all instances of + * this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])}, + * which uses a prefix tree to give better performance. + */ +class ByteArrayFilterGroup extends FilterGroup { + + private volatile int[][] failurePatterns; + + // Modified implementation from https://stackoverflow.com/a/1507813 + private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) { + // Finds the first occurrence of the pattern in the byte array using + // KMP matching algorithm. + int patternLength = pattern.length; + for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) { + while (j > 0 && pattern[j] != data[i]) { + j = failure[j - 1]; + } + if (pattern[j] == data[i]) { + j++; + } + if (j == patternLength) { + return i - patternLength + 1; + } + } + return -1; + } + + private static int[] createFailurePattern(byte[] pattern) { + // Computes the failure function using a boot-strapping process, + // where the pattern is matched against itself. + final int patternLength = pattern.length; + final int[] failure = new int[patternLength]; + + for (int i = 1, j = 0; i < patternLength; i++) { + while (j > 0 && pattern[j] != pattern[i]) { + j = failure[j - 1]; + } + if (pattern[j] == pattern[i]) { + j++; + } + failure[i] = j; + } + return failure; + } + + public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) { + super(setting, filters); + } + + /** + * Converts the Strings into byte arrays. Used to search for text in binary data. + */ + public ByteArrayFilterGroup(BooleanSetting setting, String... filters) { + super(setting, ByteTrieSearch.convertStringsToBytes(filters)); + } + + private synchronized void buildFailurePatterns() { + if (failurePatterns != null) return; // Thread race and another thread already initialized the search. + Logger.printDebug(() -> "Building failure array for: " + this); + int[][] failurePatterns = new int[filters.length][]; + int i = 0; + for (byte[] pattern : filters) { + failurePatterns[i++] = createFailurePattern(pattern); + } + this.failurePatterns = failurePatterns; // Must set after initialization finishes. + } + + @Override + public FilterGroupResult check(final byte[] bytes) { + int matchedLength = 0; + int matchedIndex = -1; + if (isEnabled()) { + int[][] failures = failurePatterns; + if (failures == null) { + buildFailurePatterns(); // Lazy load. + failures = failurePatterns; + } + for (int i = 0, length = filters.length; i < length; i++) { + byte[] filter = filters[i]; + matchedIndex = indexOf(bytes, filter, failures[i]); + if (matchedIndex >= 0) { + matchedLength = filter.length; + break; + } + } + } + return new FilterGroupResult(setting, matchedIndex, matchedLength); + } +} + diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroupList.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroupList.java new file mode 100644 index 0000000000..9babba0e2f --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroupList.java @@ -0,0 +1,85 @@ +package app.revanced.integrations.youtube.patches.components; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import java.util.*; +import java.util.function.Consumer; + +import app.revanced.integrations.youtube.ByteTrieSearch; +import app.revanced.integrations.youtube.StringTrieSearch; +import app.revanced.integrations.youtube.TrieSearch; + +abstract class FilterGroupList> implements Iterable { + + private final List filterGroups = new ArrayList<>(); + private final TrieSearch search = createSearchGraph(); + + @SafeVarargs + protected final void addAll(final T... groups) { + filterGroups.addAll(Arrays.asList(groups)); + + for (T group : groups) { + if (!group.includeInSearch()) { + continue; + } + for (V pattern : group.filters) { + search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { + if (group.isEnabled()) { + FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter; + result.setValues(group.setting, matchedStartIndex, matchedLength); + return true; + } + return false; + }); + } + } + } + + @NonNull + @Override + public Iterator iterator() { + return filterGroups.iterator(); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public void forEach(@NonNull Consumer action) { + filterGroups.forEach(action); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @NonNull + @Override + public Spliterator spliterator() { + return filterGroups.spliterator(); + } + + protected FilterGroup.FilterGroupResult check(V stack) { + FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult(); + search.matches(stack, result); + return result; + + } + + protected abstract TrieSearch createSearchGraph(); +} + +final class StringFilterGroupList extends FilterGroupList { + protected StringTrieSearch createSearchGraph() { + return new StringTrieSearch(); + } +} + +/** + * If searching for a single byte pattern, then it is slightly better to use + * {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster + * than a prefix tree to search for only 1 pattern. + */ +final class ByteArrayFilterGroupList extends FilterGroupList { + protected ByteTrieSearch createSearchGraph() { + return new ByteTrieSearch(); + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java index dc65eb3b06..211f959d09 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java @@ -1,389 +1,15 @@ package app.revanced.integrations.youtube.patches.components; -import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; import java.util.List; -import java.util.Spliterator; -import java.util.function.Consumer; import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.settings.BooleanSetting; -import app.revanced.integrations.shared.settings.BaseSettings; -import app.revanced.integrations.youtube.ByteTrieSearch; import app.revanced.integrations.youtube.StringTrieSearch; -import app.revanced.integrations.youtube.TrieSearch; import app.revanced.integrations.youtube.settings.Settings; -abstract class FilterGroup { - final static class FilterGroupResult { - private BooleanSetting setting; - private int matchedIndex; - private int matchedLength; - // In the future it might be useful to include which pattern matched, - // but for now that is not needed. - - FilterGroupResult() { - this(null, -1, 0); - } - - FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) { - setValues(setting, matchedIndex, matchedLength); - } - - public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) { - this.setting = setting; - this.matchedIndex = matchedIndex; - this.matchedLength = matchedLength; - } - - /** - * A null value if the group has no setting, - * or if no match is returned from {@link FilterGroupList#check(Object)}. - */ - public BooleanSetting getSetting() { - return setting; - } - - public boolean isFiltered() { - return matchedIndex >= 0; - } - - /** - * Matched index of first pattern that matched, or -1 if nothing matched. - */ - public int getMatchedIndex() { - return matchedIndex; - } - - /** - * Length of the matched filter pattern. - */ - public int getMatchedLength() { - return matchedLength; - } - } - - protected final BooleanSetting setting; - protected final T[] filters; - - /** - * Initialize a new filter group. - * - * @param setting The associated setting. - * @param filters The filters. - */ - @SafeVarargs - public FilterGroup(final BooleanSetting setting, final T... filters) { - this.setting = setting; - this.filters = filters; - if (filters.length == 0) { - throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)"); - } - } - - public boolean isEnabled() { - return setting == null || setting.get(); - } - - /** - * @return If {@link FilterGroupList} should include this group when searching. - * By default, all filters are included except non enabled settings that require reboot. - */ - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean includeInSearch() { - return isEnabled() || !setting.rebootApp; - } - - @NonNull - @Override - public String toString() { - return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting); - } - - public abstract FilterGroupResult check(final T stack); -} - -class StringFilterGroup extends FilterGroup { - - public StringFilterGroup(final BooleanSetting setting, final String... filters) { - super(setting, filters); - } - - @Override - public FilterGroupResult check(final String string) { - int matchedIndex = -1; - int matchedLength = 0; - if (isEnabled()) { - for (String pattern : filters) { - if (!string.isEmpty()) { - final int indexOf = string.indexOf(pattern); - if (indexOf >= 0) { - matchedIndex = indexOf; - matchedLength = pattern.length(); - break; - } - } - } - } - return new FilterGroupResult(setting, matchedIndex, matchedLength); - } -} - -/** - * If you have more than 1 filter patterns, then all instances of - * this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])}, - * which uses a prefix tree to give better performance. - */ -class ByteArrayFilterGroup extends FilterGroup { - - private volatile int[][] failurePatterns; - - // Modified implementation from https://stackoverflow.com/a/1507813 - private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) { - // Finds the first occurrence of the pattern in the byte array using - // KMP matching algorithm. - int patternLength = pattern.length; - for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) { - while (j > 0 && pattern[j] != data[i]) { - j = failure[j - 1]; - } - if (pattern[j] == data[i]) { - j++; - } - if (j == patternLength) { - return i - patternLength + 1; - } - } - return -1; - } - - private static int[] createFailurePattern(byte[] pattern) { - // Computes the failure function using a boot-strapping process, - // where the pattern is matched against itself. - final int patternLength = pattern.length; - final int[] failure = new int[patternLength]; - - for (int i = 1, j = 0; i < patternLength; i++) { - while (j > 0 && pattern[j] != pattern[i]) { - j = failure[j - 1]; - } - if (pattern[j] == pattern[i]) { - j++; - } - failure[i] = j; - } - return failure; - } - - public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) { - super(setting, filters); - } - - /** - * Converts the Strings into byte arrays. Used to search for text in binary data. - */ - public ByteArrayFilterGroup(BooleanSetting setting, String... filters) { - super(setting, ByteTrieSearch.convertStringsToBytes(filters)); - } - - private synchronized void buildFailurePatterns() { - if (failurePatterns != null) return; // Thread race and another thread already initialized the search. - Logger.printDebug(() -> "Building failure array for: " + this); - int[][] failurePatterns = new int[filters.length][]; - int i = 0; - for (byte[] pattern : filters) { - failurePatterns[i++] = createFailurePattern(pattern); - } - this.failurePatterns = failurePatterns; // Must set after initialization finishes. - } - - @Override - public FilterGroupResult check(final byte[] bytes) { - int matchedLength = 0; - int matchedIndex = -1; - if (isEnabled()) { - int[][] failures = failurePatterns; - if (failures == null) { - buildFailurePatterns(); // Lazy load. - failures = failurePatterns; - } - for (int i = 0, length = filters.length; i < length; i++) { - byte[] filter = filters[i]; - matchedIndex = indexOf(bytes, filter, failures[i]); - if (matchedIndex >= 0) { - matchedLength = filter.length; - break; - } - } - } - return new FilterGroupResult(setting, matchedIndex, matchedLength); - } -} - - -abstract class FilterGroupList> implements Iterable { - - private final List filterGroups = new ArrayList<>(); - private final TrieSearch search = createSearchGraph(); - - @SafeVarargs - protected final void addAll(final T... groups) { - filterGroups.addAll(Arrays.asList(groups)); - - for (T group : groups) { - if (!group.includeInSearch()) { - continue; - } - for (V pattern : group.filters) { - search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { - if (group.isEnabled()) { - FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter; - result.setValues(group.setting, matchedStartIndex, matchedLength); - return true; - } - return false; - }); - } - } - } - - @NonNull - @Override - public Iterator iterator() { - return filterGroups.iterator(); - } - - @RequiresApi(api = Build.VERSION_CODES.N) - @Override - public void forEach(@NonNull Consumer action) { - filterGroups.forEach(action); - } - - @RequiresApi(api = Build.VERSION_CODES.N) - @NonNull - @Override - public Spliterator spliterator() { - return filterGroups.spliterator(); - } - - protected FilterGroup.FilterGroupResult check(V stack) { - FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult(); - search.matches(stack, result); - return result; - - } - - protected abstract TrieSearch createSearchGraph(); -} - -final class StringFilterGroupList extends FilterGroupList { - protected StringTrieSearch createSearchGraph() { - return new StringTrieSearch(); - } -} - -/** - * If searching for a single byte pattern, then it is slightly better to use - * {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster - * than a prefix tree to search for only 1 pattern. - */ -final class ByteArrayFilterGroupList extends FilterGroupList { - protected ByteTrieSearch createSearchGraph() { - return new ByteTrieSearch(); - } -} - -/** - * Filters litho based components. - * - * Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)} - * and {@link #addPathCallbacks(StringFilterGroup...)}. - * - * To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to - * either an identifier or a path. - * Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} - * search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern) - * or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern). - * - * All callbacks must be registered before the constructor completes. - */ -abstract class Filter { - - public enum FilterContentType { - IDENTIFIER, - PATH, - PROTOBUFFER - } - - /** - * Identifier callbacks. Do not add to this instance, - * and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}. - */ - protected final List identifierCallbacks = new ArrayList<>(); - /** - * Path callbacks. Do not add to this instance, - * and instead use {@link #addPathCallbacks(StringFilterGroup...)}. - */ - protected final List pathCallbacks = new ArrayList<>(); - - /** - * Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} - * if any of the groups are found. - */ - protected final void addIdentifierCallbacks(StringFilterGroup... groups) { - identifierCallbacks.addAll(Arrays.asList(groups)); - } - - /** - * Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} - * if any of the groups are found. - */ - protected final void addPathCallbacks(StringFilterGroup... groups) { - pathCallbacks.addAll(Arrays.asList(groups)); - } - - /** - * Called after an enabled filter has been matched. - * Default implementation is to always filter the matched component and log the action. - * Subclasses can perform additional or different checks if needed. - *

- * If the content is to be filtered, subclasses should always - * call this method (and never return a plain 'true'). - * That way the logs will always show when a component was filtered and which filter hide it. - *

- * Method is called off the main thread. - * - * @param matchedGroup The actual filter that matched. - * @param contentType The type of content matched. - * @param contentIndex Matched index of the identifier or path. - * @return True if the litho component should be filtered out. - */ - boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { - if (BaseSettings.DEBUG.get()) { - String filterSimpleName = getClass().getSimpleName(); - if (contentType == FilterContentType.IDENTIFIER) { - Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier); - } else { - Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path); - } - } - return true; - } -} - -/** - * Placeholder for actual filters. - */ -final class DummyFilter extends Filter { } - @SuppressWarnings("unused") public final class LithoFilterPatch { /** @@ -572,4 +198,9 @@ public static void filter(@Nullable String lithoIdentifier, @NonNull StringBuild setFilteredState(false); } -} \ No newline at end of file +} + +/** + * Placeholder for actual filters. + */ +final class DummyFilter extends Filter { } \ No newline at end of file From a8ef9e2a3875d343d6a24afcb589ddea7e105d91 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 10 Sep 2024 18:02:43 -0400 Subject: [PATCH 47/91] fix: Use two method patching only with 19.18+ --- .../patches/components/LithoFilterPatch.java | 27 ++++--------------- .../youtube/requests/Requester.java | 2 +- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java index 211f959d09..5664ea8d7f 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java @@ -87,7 +87,6 @@ private static void findAsciiStrings(StringBuilder builder, byte[] buffer) { * the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads. */ private static final ThreadLocal bufferThreadLocal = new ThreadLocal<>(); - private static final ThreadLocal filteredState = new ThreadLocal<>(); static { for (Filter filter : filters) { @@ -141,28 +140,14 @@ public static void setProtoBuffer(@Nullable ByteBuffer protobufBuffer) { } } - private static void setFilteredState(boolean filtered) { - filteredState.set(filtered); - } - - /** - * Injection point. - */ - @SuppressWarnings("unused") - public static boolean filterState() { - Boolean state = filteredState.get(); - return state != null && state; - } - /** * Injection point. Called off the main thread, and commonly called by multiple threads at the same time. */ @SuppressWarnings("unused") - public static void filter(@Nullable String lithoIdentifier, @NonNull StringBuilder pathBuilder) { + public static boolean filter(@Nullable String lithoIdentifier, @NonNull StringBuilder pathBuilder) { try { if (pathBuilder.length() == 0) { - setFilteredState(false); - return; + return false; } ByteBuffer protobufBuffer = bufferThreadLocal.get(); @@ -184,19 +169,17 @@ public static void filter(@Nullable String lithoIdentifier, @NonNull StringBuild Logger.printDebug(() -> "Searching " + parameter); if (parameter.identifier != null && identifierSearchTree.matches(parameter.identifier, parameter)) { - setFilteredState(true); - return; + return true; } if (pathSearchTree.matches(parameter.path, parameter)) { - setFilteredState(true); - return; + return true; } } catch (Exception ex) { Logger.printException(() -> "Litho filter failure", ex); } - setFilteredState(false); + return false; } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java b/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java index c62e34f591..d4c8e532ce 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java @@ -44,7 +44,7 @@ private static String parseInputStreamAndClose(InputStream inputStream) throws I String line; while ((line = reader.readLine()) != null) { jsonBuilder.append(line); - jsonBuilder.append("\n"); + jsonBuilder.append('\n'); } return jsonBuilder.toString(); } From c390e2bab0d4566d347d033dd6f5909ce490cd9d Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 11 Sep 2024 21:55:14 -0400 Subject: [PATCH 48/91] feat: Initial support for `19.36.37`. Miniplayer settings need to be figured out, as using some overrides without overs gives a half broken miniplayer. --- .../youtube/patches/MiniplayerPatch.java | 58 ++++++++++++++++++- .../youtube/settings/Settings.java | 8 ++- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java index 2667cf2f4b..280810d48b 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java @@ -16,7 +16,7 @@ import app.revanced.integrations.shared.settings.Setting; import app.revanced.integrations.youtube.settings.Settings; -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "SpellCheckingInspection"}) public final class MiniplayerPatch { /** @@ -29,7 +29,12 @@ public enum MiniplayerType { TABLET(true, null), MODERN_1(null, 1), MODERN_2(null, 2), - MODERN_3(null, 3); + MODERN_3(null, 3), + /** + * Half broken miniplayer, that might be work in progress or left over abandoned code. + * Can force this type by editing the import/export settings. + */ + MODERN_4(null, 4); /** * Legacy tablet hook value. @@ -109,6 +114,8 @@ public boolean isModern() { private static final boolean HIDE_REWIND_FORWARD_ENABLED = CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get(); + private static final boolean DROP_SHADOW = Settings.MINIPLAYER_DROP_SHADOW.get(); + private static final int OPACITY_LEVEL; public static final class MiniplayerHideExpandCloseAvailability implements Setting.Availability { @@ -170,10 +177,24 @@ public static void adjustMiniplayerOpacity(ImageView view) { } } + /** + * Injection point. + */ + public static boolean getModernFeatureFlagsActiveOverride(boolean original) { + Logger.printDebug(() -> "getModernFeatureFlagsActiveOverride original: " + original); + + // This must be on to allow other feature flags to be used, + // but if always set to 'true' this breaks the miniplayer when some features are not on. + // TODO: Figure out when to enable this. + return true; + } + /** * Injection point. */ public static boolean enableMiniplayerDoubleTapAction(boolean original) { + Logger.printDebug(() -> "enableMiniplayerDoubleTapAction original: " + original); + return DOUBLE_TAP_ACTION_ENABLED; } @@ -181,13 +202,15 @@ public static boolean enableMiniplayerDoubleTapAction(boolean original) { * Injection point. */ public static boolean enableMiniplayerDragAndDrop(boolean original) { + Logger.printDebug(() -> "enableMiniplayerDragAndDrop original: " + original); + return DRAG_AND_DROP_ENABLED; } /** * Injection point. */ - public static int setMiniplayerSize(int original) { + public static int setMiniplayerDefaultSize(int original) { if (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_2 || CURRENT_TYPE == MODERN_3) { return MINIPLAYER_SIZE; } @@ -195,6 +218,35 @@ public static int setMiniplayerSize(int original) { return original; } + /** + * Injection point. + */ + public static float setMovementBoundFactor(float original) { + // Not clear if this is useful to customize or not. + // So for now just log this and keep whatever is the original value. + Logger.printDebug(() -> "setMovementBoundFactor original: " + original); + + return original; + } + + /** + * Injection point. + */ + public static boolean setDropShadow(boolean original) { + Logger.printDebug(() -> "setViewElevation original: " + original); + + return DROP_SHADOW; + } + + /** + * Injection point. + */ + public static boolean setUseBackgroundViewOutlineProvider(boolean original) { + Logger.printDebug(() -> "setUseBackgroundViewOutlineProvider original: " + original); + + return original; + } + /** * Injection point. */ diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index bbdf35293d..d8a95d0854 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -134,12 +134,14 @@ public class Settings extends BaseSettings { // Miniplayer public static final EnumSetting MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.ORIGINAL, true); - public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3)); - public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_enable_double_tap_action", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3)); - public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_enable_drag_and_drop", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1)); + private static final Availability MINIPLAYER_ANY_MODERN = MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3, MODERN_4); + public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_double_tap_action", TRUE, true, MINIPLAYER_ANY_MODERN); + public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_drag_and_drop", TRUE, true, MINIPLAYER_TYPE.availability(MODERN_1)); public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true, new MiniplayerHideExpandCloseAvailability()); public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3)); public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1)); + public static final BooleanSetting MINIPLAYER_DROP_SHADOW = new BooleanSetting("revanced_miniplayer_drop_shadow", TRUE, true, MINIPLAYER_ANY_MODERN); + public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, MINIPLAYER_ANY_MODERN); public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, MINIPLAYER_TYPE.availability(MODERN_1)); // External downloader From 6b8f097bda80829aba42a9e64fb32bf3463ab3f9 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 13 Sep 2024 22:54:29 -0700 Subject: [PATCH 49/91] fix miniplayer: Only recommend versions that have no bugs, and have specific reasons to use over other versions. --- .../youtube/patches/MiniplayerPatch.java | 45 ++++++++++++------- .../youtube/settings/Settings.java | 3 +- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java index 280810d48b..fa3d4ccf89 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java @@ -88,6 +88,11 @@ public boolean isModern() { MINIPLAYER_SIZE = dipWidth; } + private static final boolean IS_19_20_OR_GREATER = Utils.getAppVersionName().compareTo("19.20.00") >= 0; + private static final boolean IS_19_21_OR_GREATER = Utils.getAppVersionName().compareTo("19.21.00") >= 0; + private static final boolean IS_19_26_OR_GREATER = Utils.getAppVersionName().compareTo("19.26.00") >= 0; + private static final boolean IS_19_29_OR_GREATER = Utils.getAppVersionName().compareTo("19.29.00") >= 0; + /** * Modern subtitle overlay for {@link MiniplayerType#MODERN_2}. * Resource is not present in older targets, and this field will be zero. @@ -97,16 +102,21 @@ public boolean isModern() { private static final MiniplayerType CURRENT_TYPE = Settings.MINIPLAYER_TYPE.get(); + /** + * Cannot turn off double tap with modern 2 or 3 with later targets, + * as forcing it off breakings tapping the miniplayer. + */ private static final boolean DOUBLE_TAP_ACTION_ENABLED = - (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_2 || CURRENT_TYPE == MODERN_3) - && Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get(); + // 19.29+ is very broken if double tap is not enabled. + IS_19_29_OR_GREATER || + (CURRENT_TYPE.isModern() & Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get()); private static final boolean DRAG_AND_DROP_ENABLED = - CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_DRAG_AND_DROP.get(); + CURRENT_TYPE.isModern() && Settings.MINIPLAYER_DRAG_AND_DROP.get(); private static final boolean HIDE_EXPAND_CLOSE_ENABLED = - (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) - && !DRAG_AND_DROP_ENABLED && Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get(); + Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get() + && Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.isAvailable(); private static final boolean HIDE_SUBTEXT_ENABLED = (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_SUBTEXT.get(); @@ -114,16 +124,23 @@ public boolean isModern() { private static final boolean HIDE_REWIND_FORWARD_ENABLED = CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get(); - private static final boolean DROP_SHADOW = Settings.MINIPLAYER_DROP_SHADOW.get(); + /** + * Remove a broken and always present subtitle text that is only + * present with {@link MiniplayerType#MODERN_2}. Bug was fixed in 19.21. + */ + private static final boolean HIDE_BROKEN_MODERN_2_SUBTITLE = + CURRENT_TYPE == MODERN_2 && !IS_19_21_OR_GREATER; private static final int OPACITY_LEVEL; public static final class MiniplayerHideExpandCloseAvailability implements Setting.Availability { - Setting.Availability modernOneOrThree = Settings.MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3); - @Override public boolean isAvailable() { - return modernOneOrThree.isAvailable() && !Settings.MINIPLAYER_DRAG_AND_DROP.get(); + MiniplayerType type = Settings.MINIPLAYER_TYPE.get(); + return (!IS_19_20_OR_GREATER && (type == MODERN_1 || type == MODERN_3)) + || (!IS_19_26_OR_GREATER && type == MODERN_1 + && !Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get() && !Settings.MINIPLAYER_DRAG_AND_DROP.get()) + || (IS_19_29_OR_GREATER && type == MODERN_3); } } @@ -183,9 +200,7 @@ public static void adjustMiniplayerOpacity(ImageView view) { public static boolean getModernFeatureFlagsActiveOverride(boolean original) { Logger.printDebug(() -> "getModernFeatureFlagsActiveOverride original: " + original); - // This must be on to allow other feature flags to be used, - // but if always set to 'true' this breaks the miniplayer when some features are not on. - // TODO: Figure out when to enable this. + // This should always be true if either double tap or drag and drop are enabled. return true; } @@ -235,7 +250,7 @@ public static float setMovementBoundFactor(float original) { public static boolean setDropShadow(boolean original) { Logger.printDebug(() -> "setViewElevation original: " + original); - return DROP_SHADOW; + return original; } /** @@ -281,9 +296,7 @@ public static void hideMiniplayerSubTexts(View view) { */ public static void playerOverlayGroupCreated(View group) { try { - // Modern 2 has an half broken subtitle that is always present. - // Always hide it to make the miniplayer mostly usable. - if (CURRENT_TYPE == MODERN_2 && MODERN_OVERLAY_SUBTITLE_TEXT != 0) { + if (HIDE_BROKEN_MODERN_2_SUBTITLE && MODERN_OVERLAY_SUBTITLE_TEXT != 0) { if (group instanceof ViewGroup) { View subtitleText = Utils.getChildView((ViewGroup) group, true, view -> view.getId() == MODERN_OVERLAY_SUBTITLE_TEXT); diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index d8a95d0854..9571b36ebb 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -136,11 +136,10 @@ public class Settings extends BaseSettings { public static final EnumSetting MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.ORIGINAL, true); private static final Availability MINIPLAYER_ANY_MODERN = MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3, MODERN_4); public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_double_tap_action", TRUE, true, MINIPLAYER_ANY_MODERN); - public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_drag_and_drop", TRUE, true, MINIPLAYER_TYPE.availability(MODERN_1)); + public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_drag_and_drop", TRUE, true, MINIPLAYER_ANY_MODERN); public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true, new MiniplayerHideExpandCloseAvailability()); public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3)); public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1)); - public static final BooleanSetting MINIPLAYER_DROP_SHADOW = new BooleanSetting("revanced_miniplayer_drop_shadow", TRUE, true, MINIPLAYER_ANY_MODERN); public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, MINIPLAYER_ANY_MODERN); public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, MINIPLAYER_TYPE.availability(MODERN_1)); From 33bee7283dc1a52698aa8af5afb935fadd11a980 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 13 Sep 2024 23:12:07 -0700 Subject: [PATCH 50/91] Revert "Merge branch 'refs/heads/fix/yt-spoof-stream' into feat/yt-bump" This reverts commit 444bb6b3a131e610ecc721c67c314f9fb98ac229, reversing changes made to d6782c55d4dfb7f628a5a3c8973c98ea92f3145b. --- .../youtube/patches/spoof/ClientType.java | 79 ------ .../patches/spoof/DeviceHardwareSupport.java | 53 ---- .../patches/spoof/SpoofClientPatch.java | 255 +++++++++++++----- .../patches/spoof/SpoofSignaturePatch.java | 242 +++++++++++++++++ .../patches/spoof/StoryboardRenderer.java | 36 +++ .../patches/spoof/requests/PlayerRoutes.java | 80 ++++-- .../requests/StoryboardRendererRequester.java | 153 +++++++++++ .../spoof/requests/StreamingDataRequest.java | 177 ------------ .../youtube/settings/Settings.java | 7 +- .../chromium/net/ExperimentalUrlRequest.java | 8 + .../java/org/chromium/net/UrlRequest.java | 4 - 11 files changed, 684 insertions(+), 410 deletions(-) delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java create mode 100644 stub/src/main/java/org/chromium/net/ExperimentalUrlRequest.java diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java deleted file mode 100644 index f5300cb87c..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java +++ /dev/null @@ -1,79 +0,0 @@ -package app.revanced.integrations.youtube.patches.spoof; - -import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.allowAV1; -import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.allowVP9; - -import android.os.Build; - -import androidx.annotation.Nullable; - -public enum ClientType { - // https://dumps.tadiphone.dev/dumps/oculus/eureka - IOS(5, - // iPhone 15 supports AV1 hardware decoding. - // Only use if this Android device also has hardware decoding. - allowAV1() - ? "iPhone16,2" // 15 Pro Max - : "iPhone11,4", // XS Max - // iOS 14+ forces VP9. - allowVP9() - ? "17.5.1.21F90" - : "13.7.17H35", - allowVP9() - ? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)" - : "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)", - null, - // Version number should be a valid iOS release. - // https://www.ipa4fun.com/history/185230 - "19.10.7" - ), - ANDROID_VR(28, - "Quest 3", - "12", - "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", - "32", // Android 12.1 - "1.56.21" - ); - - /** - * YouTube - * client type - */ - public final int id; - - /** - * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) - */ - public final String model; - - /** - * Device OS version. - */ - public final String osVersion; - - /** - * Player user-agent. - */ - public final String userAgent; - - /** - * Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk) - * Field is null if not applicable. - */ - @Nullable - public final String androidSdkVersion; - - /** - * App version. - */ - public final String appVersion; - - ClientType(int id, String model, String osVersion, String userAgent, @Nullable String androidSdkVersion, String appVersion) { - this.id = id; - this.model = model; - this.osVersion = osVersion; - this.userAgent = userAgent; - this.androidSdkVersion = androidSdkVersion; - this.appVersion = appVersion; - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java deleted file mode 100644 index 2eead95753..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java +++ /dev/null @@ -1,53 +0,0 @@ -package app.revanced.integrations.youtube.patches.spoof; - -import android.media.MediaCodecInfo; -import android.media.MediaCodecList; -import android.os.Build; - -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.youtube.settings.Settings; - -public class DeviceHardwareSupport { - private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9; - private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1; - - static { - boolean vp9found = false; - boolean av1found = false; - MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); - final boolean deviceIsAndroidTenOrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; - - for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - final boolean isHardwareAccelerated = deviceIsAndroidTenOrLater - ? codecInfo.isHardwareAccelerated() - : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. - if (isHardwareAccelerated && !codecInfo.isEncoder()) { - for (String type : codecInfo.getSupportedTypes()) { - if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { - vp9found = true; - } else if (type.equalsIgnoreCase("video/av01")) { - av1found = true; - } - } - } - } - - DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found; - DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found; - - Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1 - ? "Device supports AV1 hardware decoding\n" - : "Device does not support AV1 hardware decoding\n" - + (DEVICE_HAS_HARDWARE_DECODING_VP9 - ? "Device supports VP9 hardware decoding" - : "Device does not support VP9 hardware decoding")); - } - - public static boolean allowVP9() { - return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_CLIENT_FORCE_AVC.get(); - } - - public static boolean allowAV1() { - return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1; - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index ffa99760ee..2b4f04d863 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -1,22 +1,25 @@ package app.revanced.integrations.youtube.patches.spoof; -import android.net.Uri; - -import androidx.annotation.Nullable; +import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.DeviceHardwareSupport.allowAV1; +import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.DeviceHardwareSupport.allowVP9; -import org.chromium.net.UrlRequest; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.net.Uri; +import android.os.Build; -import java.nio.ByteBuffer; -import java.util.Map; -import java.util.Objects; +import org.chromium.net.ExperimentalUrlRequest; import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequest; +import app.revanced.integrations.shared.settings.Setting; +import app.revanced.integrations.youtube.patches.BackgroundPlaybackPatch; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") public class SpoofClientPatch { - private static final boolean SPOOF_CLIENT = Settings.SPOOF_CLIENT.get(); + private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get(); + private static final ClientType SPOOF_CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get(); + private static final boolean SPOOF_IOS = SPOOF_CLIENT_ENABLED && SPOOF_CLIENT_TYPE == ClientType.IOS; /** * Any unreachable ip address. Used to intentionally fail requests. @@ -32,7 +35,7 @@ public class SpoofClientPatch { * @return An unreachable URI if the request is a /get_watch request, otherwise the original URI. */ public static Uri blockGetWatchRequest(Uri playerRequestUri) { - if (SPOOF_CLIENT) { + if (SPOOF_CLIENT_ENABLED) { try { String path = playerRequestUri.getPath(); @@ -55,7 +58,7 @@ public static Uri blockGetWatchRequest(Uri playerRequestUri) { * Blocks /initplayback requests. */ public static String blockInitPlaybackRequest(String originalUrlString) { - if (SPOOF_CLIENT) { + if (SPOOF_CLIENT_ENABLED) { try { var originalUri = Uri.parse(originalUrlString); String path = originalUri.getPath(); @@ -76,85 +79,201 @@ public static String blockInitPlaybackRequest(String originalUrlString) { /** * Injection point. */ - public static boolean isSpoofingEnabled() { - return SPOOF_CLIENT; + public static int getClientTypeId(int originalClientTypeId) { + return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.id : originalClientTypeId; } /** * Injection point. */ - public static UrlRequest buildRequest(UrlRequest.Builder builder, String url, - Map playerHeaders) { - if (SPOOF_CLIENT) { - try { - Uri uri = Uri.parse(url); - String path = uri.getPath(); - if (path != null && path.contains("player") && !path.contains("heartbeat")) { - String videoId = Objects.requireNonNull(uri.getQueryParameter("id")); - StreamingDataRequest.fetchRequest(videoId, playerHeaders); - } - } catch (Exception ex) { - Logger.printException(() -> "buildRequest failure", ex); - } - } + public static String getClientVersion(String originalClientVersion) { + return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.appVersion : originalClientVersion; + } - return builder.build(); + /** + * Injection point. + */ + public static String getClientModel(String originalClientModel) { + return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.model : originalClientModel; } /** * Injection point. - * Fix playback by replace the streaming data. - * Called after {@link #buildRequest(UrlRequest.Builder, String, Map)}. + * Fix video qualities missing, if spoofing to iOS by using the correct client OS version. */ - @Nullable - public static ByteBuffer getStreamingData(String videoId) { - if (SPOOF_CLIENT) { - try { - // This hook is always called off the main thread, - // but this can later be called for the same video id from the main thread. - // This is not a concern, since the fetch will always be finished - // and never block the main thread. - - StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId); - if (request != null) { - var stream = request.getStream(); - if (stream != null) { - Logger.printDebug(() -> "Overriding video stream: " + videoId); - return stream; - } - } + public static String getOsVersion(String originalOsVersion) { + return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.osVersion : originalOsVersion; + } - Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId); - } catch (Exception ex) { - Logger.printException(() -> "getStreamingData failure", ex); - } - } + /** + * Injection point. + */ + public static boolean enablePlayerGesture(boolean original) { + return SPOOF_CLIENT_ENABLED || original; + } - return null; + /** + * Injection point. + */ + public static boolean isClientSpoofingEnabled() { + return SPOOF_CLIENT_ENABLED; } /** * Injection point. - * Called after {@link #getStreamingData(String)}. + * When spoofing the client to iOS, the playback speed menu is missing from the player response. + * Return true to force create the playback speed menu. */ - @Nullable - public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) { - if (SPOOF_CLIENT) { - try { - final int methodPost = 2; - if (method == methodPost) { - String path = uri.getPath(); - String clientName = "c"; - final boolean iosClient = ClientType.IOS.name().equals(uri.getQueryParameter(clientName)); - if (iosClient && path != null && path.contains("videoplayback")) { - return null; + public static boolean forceCreatePlaybackSpeedMenu(boolean original) { + return SPOOF_IOS || original; + } + + /** + * Injection point. + * When spoofing the client to iOS, background audio only playback of livestreams fails. + * Return true to force enable audio background play. + */ + public static boolean overrideBackgroundAudioPlayback() { + return SPOOF_IOS && BackgroundPlaybackPatch.allowBackgroundPlayback(false); + } + + /** + * Injection point. + * Fix video qualities missing, if spoofing to iOS by using the correct iOS user-agent. + */ + public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Builder builder, String url) { + if (SPOOF_CLIENT_ENABLED) { + String path = Uri.parse(url).getPath(); + if (path != null && path.contains("player")) { + return builder.addHeader("User-Agent", SPOOF_CLIENT_TYPE.userAgent).build(); + } + } + + return builder.build(); + } + + // Must check for device features in a separate class and cannot place this code inside + // the Patch or ClientType enum due to cyclic Setting references. + static class DeviceHardwareSupport { + private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding(); + private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding(); + + private static boolean deviceHasVP9HardwareDecoding() { + MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + + for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { + final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + ? codecInfo.isHardwareAccelerated() + : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. + if (isHardwareAccelerated && !codecInfo.isEncoder()) { + for (String type : codecInfo.getSupportedTypes()) { + if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { + Logger.printDebug(() -> "Device supports VP9 hardware decoding."); + return true; + } + } + } + } + + Logger.printDebug(() -> "Device does not support VP9 hardware decoding."); + return false; + } + + private static boolean deviceHasAV1HardwareDecoding() { + // It appears all devices with hardware AV1 are also Android 10 or newer. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + + for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { + if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) { + for (String type : codecInfo.getSupportedTypes()) { + if (type.equalsIgnoreCase("video/av01")) { + Logger.printDebug(() -> "Device supports AV1 hardware decoding."); + return true; + } + } } } - } catch (Exception ex) { - Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex); } + + Logger.printDebug(() -> "Device does not support AV1 hardware decoding."); + return false; } - return postData; + static boolean allowVP9() { + return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_CLIENT_IOS_FORCE_AVC.get(); + } + + static boolean allowAV1() { + return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1; + } + } + + public enum ClientType { + // https://dumps.tadiphone.dev/dumps/oculus/eureka + IOS(5, + // iPhone 15 supports AV1 hardware decoding. + // Only use if this Android device also has hardware decoding. + allowAV1() + ? "iPhone16,2" // 15 Pro Max + : "iPhone11,4", // XS Max + // iOS 14+ forces VP9. + allowVP9() + ? "17.5.1.21F90" + : "13.7.17H35", + allowVP9() + ? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)" + : "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)", + // Version number should be a valid iOS release. + // https://www.ipa4fun.com/history/185230 + "19.10.7" + ), + ANDROID_VR(28, + "Quest 3", + "12", + "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", + "1.56.21" + ); + + /** + * YouTube + * client type + */ + final int id; + + /** + * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) + */ + final String model; + + /** + * Device OS version. + */ + final String osVersion; + + /** + * Player user-agent. + */ + final String userAgent; + + /** + * App version. + */ + final String appVersion; + + ClientType(int id, String model, String osVersion, String userAgent, String appVersion) { + this.id = id; + this.model = model; + this.osVersion = osVersion; + this.userAgent = userAgent; + this.appVersion = appVersion; + } + } + + public static final class ForceiOSAVCAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return Settings.SPOOF_CLIENT.get() && Settings.SPOOF_CLIENT_TYPE.get() == ClientType.IOS; + } } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java new file mode 100644 index 0000000000..41f03ed781 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java @@ -0,0 +1,242 @@ +package app.revanced.integrations.youtube.patches.spoof; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import androidx.annotation.Nullable; +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.youtube.patches.VideoInformation; +import app.revanced.integrations.youtube.settings.Settings; +import app.revanced.integrations.youtube.shared.PlayerType; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static app.revanced.integrations.shared.Utils.containsAny; +import static app.revanced.integrations.youtube.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer; + +/** @noinspection unused*/ +@Deprecated +public class SpoofSignaturePatch { + /** + * Parameter (also used by + * yt-dlp) + * to fix playback issues. + */ + private static final String INCOGNITO_PARAMETERS = "CgIQBg=="; + + /** + * Parameters used when playing clips. + */ + private static final String CLIPS_PARAMETERS = "kAIB"; + + /** + * Parameters causing playback issues. + */ + private static final String[] AUTOPLAY_PARAMETERS = { + "YAHI", // Autoplay in feed. + "SAFg" // Autoplay in scrim. + }; + + /** + * Parameter used for autoplay in scrim. + * Prepend this parameter to mute video playback (for autoplay in feed). + */ + private static final String SCRIM_PARAMETER = "SAFgAXgB"; + + /** + * Last video id loaded. Used to prevent reloading the same spec multiple times. + */ + @Nullable + private static volatile String lastPlayerResponseVideoId; + + @Nullable + private static volatile Future rendererFuture; + + private static volatile boolean useOriginalStoryboardRenderer; + + private static volatile boolean isPlayingShorts; + + @Nullable + private static StoryboardRenderer getRenderer(boolean waitForCompletion) { + Future future = rendererFuture; + if (future != null) { + try { + if (waitForCompletion || future.isDone()) { + return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout. + } // else, return null. + } catch (TimeoutException ex) { + Logger.printDebug(() -> "Could not get renderer (get timed out)"); + } catch (ExecutionException | InterruptedException ex) { + // Should never happen. + Logger.printException(() -> "Could not get renderer", ex); + } + } + return null; + } + + /** + * Injection point. + * + * Called off the main thread, and called multiple times for each video. + * + * @param parameters Original protobuf parameter value. + */ + public static String spoofParameter(String parameters, String videoId, boolean isShortAndOpeningOrPlaying) { + try { + Logger.printDebug(() -> "Original protobuf parameter value: " + parameters); + + if (parameters == null || !Settings.SPOOF_SIGNATURE.get()) { + return parameters; + } + + // Clip's player parameters contain a lot of information (e.g. video start and end time or whether it loops) + // For this reason, the player parameters of a clip are usually very long (150~300 characters). + // Clips are 60 seconds or less in length, so no spoofing. + //noinspection AssignmentUsedAsCondition + if (useOriginalStoryboardRenderer = parameters.length() > 150 || parameters.startsWith(CLIPS_PARAMETERS)) { + return parameters; + } + + // Shorts do not need to be spoofed. + //noinspection AssignmentUsedAsCondition + if (useOriginalStoryboardRenderer = VideoInformation.playerParametersAreShort(parameters)) { + isPlayingShorts = true; + return parameters; + } + isPlayingShorts = false; + + boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL + && containsAny(parameters, AUTOPLAY_PARAMETERS); + if (isPlayingFeed) { + //noinspection AssignmentUsedAsCondition + if (useOriginalStoryboardRenderer = !Settings.SPOOF_SIGNATURE_IN_FEED.get()) { + // Don't spoof the feed video playback. This will cause video playback issues, + // but only if user continues watching for more than 1 minute. + return parameters; + } + // Spoof the feed video. Video will show up in watch history and video subtitles are missing. + fetchStoryboardRenderer(); + return SCRIM_PARAMETER + INCOGNITO_PARAMETERS; + } + + fetchStoryboardRenderer(); + } catch (Exception ex) { + Logger.printException(() -> "spoofParameter failure", ex); + } + return INCOGNITO_PARAMETERS; + } + + private static void fetchStoryboardRenderer() { + if (!Settings.SPOOF_STORYBOARD_RENDERER.get()) { + lastPlayerResponseVideoId = null; + rendererFuture = null; + return; + } + String videoId = VideoInformation.getPlayerResponseVideoId(); + if (!videoId.equals(lastPlayerResponseVideoId)) { + rendererFuture = Utils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId)); + lastPlayerResponseVideoId = videoId; + } + // Block until the renderer fetch completes. + // This is desired because if this returns without finishing the fetch + // then video will start playback but the storyboard is not ready yet. + getRenderer(true); + } + + private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec, + boolean returnNullIfLiveStream) { + if (Settings.SPOOF_SIGNATURE.get() && !useOriginalStoryboardRenderer) { + StoryboardRenderer renderer = getRenderer(false); + if (renderer != null) { + if (returnNullIfLiveStream && renderer.isLiveStream) { + return null; + } + + if (renderer.spec != null) { + return renderer.spec; + } + } + } + + return originalStoryboardRendererSpec; + } + + /** + * Injection point. + * Called from background threads and from the main thread. + */ + @Nullable + public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) { + return getStoryboardRendererSpec(originalStoryboardRendererSpec, false); + } + + /** + * Injection point. + * Uses additional check to handle live streams. + * Called from background threads and from the main thread. + */ + @Nullable + public static String getStoryboardDecoderRendererSpec(String originalStoryboardRendererSpec) { + return getStoryboardRendererSpec(originalStoryboardRendererSpec, true); + } + + /** + * Injection point. + */ + public static int getRecommendedLevel(int originalLevel) { + if (Settings.SPOOF_SIGNATURE.get() && !useOriginalStoryboardRenderer) { + StoryboardRenderer renderer = getRenderer(false); + if (renderer != null) { + if (renderer.recommendedLevel != null) { + return renderer.recommendedLevel; + } + } + } + + return originalLevel; + } + + /** + * Injection point. Forces seekbar to be shown for paid videos or + * if {@link Settings#SPOOF_STORYBOARD_RENDERER} is not enabled. + */ + public static boolean getSeekbarThumbnailOverrideValue() { + if (!Settings.SPOOF_SIGNATURE.get()) { + return false; + } + StoryboardRenderer renderer = getRenderer(false); + if (renderer == null) { + // Spoof storyboard renderer is turned off, + // video is paid, or the storyboard fetch timed out. + // Show empty thumbnails so the seek time and chapters still show up. + return true; + } + return renderer.spec != null; + } + + /** + * Injection point. + * + * @param view seekbar thumbnail view. Includes both shorts and regular videos. + */ + public static void seekbarImageViewCreated(ImageView view) { + try { + if (!Settings.SPOOF_SIGNATURE.get() + || Settings.SPOOF_STORYBOARD_RENDERER.get()) { + return; + } + if (isPlayingShorts) return; + + view.setVisibility(View.GONE); + // Also hide the border around the thumbnail (otherwise a 1 pixel wide bordered frame is visible). + ViewGroup parentLayout = (ViewGroup) view.getParent(); + parentLayout.setPadding(0, 0, 0, 0); + } catch (Exception ex) { + Logger.printException(() -> "seekbarImageViewCreated failure", ex); + } + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java new file mode 100644 index 0000000000..5014a5fcdc --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java @@ -0,0 +1,36 @@ +package app.revanced.integrations.youtube.patches.spoof; + +import androidx.annotation.Nullable; + +import org.jetbrains.annotations.NotNull; + +@Deprecated +public final class StoryboardRenderer { + public final String videoId; + @Nullable + public final String spec; + public final boolean isLiveStream; + /** + * Recommended image quality level, or NULL if no recommendation exists. + */ + @Nullable + public final Integer recommendedLevel; + + public StoryboardRenderer(String videoId, @Nullable String spec, boolean isLiveStream, @Nullable Integer recommendedLevel) { + this.videoId = videoId; + this.spec = spec; + this.isLiveStream = isLiveStream; + this.recommendedLevel = recommendedLevel; + } + + @NotNull + @Override + public String toString() { + return "StoryboardRenderer{" + + "videoId=" + videoId + + ", isLiveStream=" + isLiveStream + + ", spec='" + spec + '\'' + + ", recommendedLevel=" + recommendedLevel + + '}'; + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java index 299110f461..1927b1d68a 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java @@ -1,68 +1,94 @@ package app.revanced.integrations.youtube.patches.spoof.requests; +import app.revanced.integrations.youtube.requests.Requester; +import app.revanced.integrations.youtube.requests.Route; +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.net.HttpURLConnection; -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.youtube.patches.spoof.ClientType; -import app.revanced.integrations.youtube.requests.Requester; -import app.revanced.integrations.youtube.requests.Route; - final class PlayerRoutes { - private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"; - - static final Route.CompiledRoute GET_STREAMING_DATA = new Route( + private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/"; + static final Route.CompiledRoute GET_STORYBOARD_SPEC_RENDERER = new Route( Route.Method.POST, "player" + - "?fields=streamingData" + - "&alt=proto" + "?fields=storyboards.playerStoryboardSpecRenderer," + + "storyboards.playerLiveStoryboardSpecRenderer," + + "playabilityStatus.status" ).compile(); + static final String ANDROID_INNER_TUBE_BODY; + static final String TV_EMBED_INNER_TUBE_BODY; + /** * TCP connection and HTTP read timeout */ private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds. - private PlayerRoutes() { - } - - static String createInnertubeBody(ClientType clientType) { - JSONObject innerTubeBody = new JSONObject(); + static { + JSONObject innerTubeBody = new JSONObject(); try { JSONObject context = new JSONObject(); JSONObject client = new JSONObject(); - client.put("clientName", clientType.name()); - client.put("clientVersion", clientType.appVersion); - client.put("deviceModel", clientType.model); - client.put("osVersion", clientType.osVersion); - if (clientType.androidSdkVersion != null) { - client.put("androidSdkVersion", clientType.androidSdkVersion); - } + client.put("clientName", "ANDROID"); + client.put("clientVersion", Utils.getAppVersionName()); + client.put("androidSdkVersion", 34); context.put("client", client); innerTubeBody.put("context", context); - innerTubeBody.put("contentCheckOk", true); - innerTubeBody.put("racyCheckOk", true); innerTubeBody.put("videoId", "%s"); } catch (JSONException e) { Logger.printException(() -> "Failed to create innerTubeBody", e); } - return innerTubeBody.toString(); + ANDROID_INNER_TUBE_BODY = innerTubeBody.toString(); + + JSONObject tvEmbedInnerTubeBody = new JSONObject(); + + try { + JSONObject context = new JSONObject(); + + JSONObject client = new JSONObject(); + client.put("clientName", "TVHTML5_SIMPLY_EMBEDDED_PLAYER"); + client.put("clientVersion", "2.0"); + client.put("platform", "TV"); + client.put("clientScreen", "EMBED"); + + JSONObject thirdParty = new JSONObject(); + thirdParty.put("embedUrl", "https://www.youtube.com/watch?v=%s"); + + context.put("thirdParty", thirdParty); + context.put("client", client); + + tvEmbedInnerTubeBody.put("context", context); + tvEmbedInnerTubeBody.put("videoId", "%s"); + } catch (JSONException e) { + Logger.printException(() -> "Failed to create tvEmbedInnerTubeBody", e); + } + + TV_EMBED_INNER_TUBE_BODY = tvEmbedInnerTubeBody.toString(); + } + + private PlayerRoutes() { } /** @noinspection SameParameterValue*/ - static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException { + static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route) throws IOException { var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route); + connection.setRequestProperty( + "User-Agent", "com.google.android.youtube/" + + Utils.getAppVersionName() + + " (Linux; U; Android 12; GB) gzip" + ); + connection.setRequestProperty("X-Goog-Api-Format-Version", "2"); connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("User-Agent", clientType.userAgent); connection.setUseCaches(false); connection.setDoOutput(true); diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java new file mode 100644 index 0000000000..0cbec19400 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java @@ -0,0 +1,153 @@ +package app.revanced.integrations.youtube.patches.spoof.requests; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import app.revanced.integrations.shared.settings.BaseSettings; +import app.revanced.integrations.youtube.patches.spoof.StoryboardRenderer; +import app.revanced.integrations.youtube.requests.Requester; +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +import static app.revanced.integrations.shared.StringRef.str; +import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.*; + +public class StoryboardRendererRequester { + + private StoryboardRendererRequester() { + } + + private static void randomlyWaitIfLocallyDebugging() { + final boolean randomlyWait = false; // Enable to simulate slow connection responses. + if (randomlyWait) { + final long maximumTimeToRandomlyWait = 10000; + Utils.doNothingForDuration(maximumTimeToRandomlyWait); + } + } + + private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex, + boolean showToastOnIOException) { + if (showToastOnIOException) Utils.showToastShort(toastMessage); + Logger.printInfo(() -> toastMessage, ex); + } + + @Nullable + private static JSONObject fetchPlayerResponse(@NonNull String requestBody, boolean showToastOnIOException) { + final long startTime = System.currentTimeMillis(); + try { + Utils.verifyOffMainThread(); + Objects.requireNonNull(requestBody); + + final byte[] innerTubeBody = requestBody.getBytes(StandardCharsets.UTF_8); + + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STORYBOARD_SPEC_RENDERER); + connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length); + + final int responseCode = connection.getResponseCode(); + randomlyWaitIfLocallyDebugging(); + if (responseCode == 200) return Requester.parseJSONObject(connection); + + // Always show a toast for this, as a non 200 response means something is broken. + // Not a normal code path and should not be reached, so no translations are needed. + handleConnectionError("Spoof storyboard not available: " + responseCode, + null, showToastOnIOException || BaseSettings.DEBUG_TOAST_ON_ERROR.get()); + connection.disconnect(); + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_spoof_client_storyboard_timeout"), ex, showToastOnIOException); + } catch (IOException ex) { + handleConnectionError(str("revanced_spoof_client_storyboard_io_exception", ex.getMessage()), + ex, showToastOnIOException); + } catch (Exception ex) { + Logger.printException(() -> "Spoof storyboard fetch failed", ex); // Should never happen. + } finally { + Logger.printDebug(() -> "Request took: " + (System.currentTimeMillis() - startTime) + "ms"); + } + + return null; + } + + private static boolean isPlayabilityStatusOk(@NonNull JSONObject playerResponse) { + try { + return playerResponse.getJSONObject("playabilityStatus").getString("status").equals("OK"); + } catch (JSONException e) { + Logger.printDebug(() -> "Failed to get playabilityStatus for response: " + playerResponse); + } + + return false; + } + + /** + * Fetches the storyboardRenderer from the innerTubeBody. + * @param innerTubeBody The innerTubeBody to use to fetch the storyboardRenderer. + * @return StoryboardRenderer or null if playabilityStatus is not OK. + */ + @Nullable + private static StoryboardRenderer getStoryboardRendererUsingBody(String videoId, + @NonNull String innerTubeBody, + boolean showToastOnIOException) { + final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody, showToastOnIOException); + if (playerResponse != null && isPlayabilityStatusOk(playerResponse)) + return getStoryboardRendererUsingResponse(videoId, playerResponse); + + return null; + } + + @Nullable + private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull String videoId, @NonNull JSONObject playerResponse) { + try { + Logger.printDebug(() -> "Parsing response: " + playerResponse); + if (!playerResponse.has("storyboards")) { + Logger.printDebug(() -> "Using empty storyboard"); + return new StoryboardRenderer(videoId, null, false, null); + } + final JSONObject storyboards = playerResponse.getJSONObject("storyboards"); + final boolean isLiveStream = storyboards.has("playerLiveStoryboardSpecRenderer"); + final String storyboardsRendererTag = isLiveStream + ? "playerLiveStoryboardSpecRenderer" + : "playerStoryboardSpecRenderer"; + + final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag); + StoryboardRenderer renderer = new StoryboardRenderer( + videoId, + rendererElement.getString("spec"), + isLiveStream, + rendererElement.has("recommendedLevel") + ? rendererElement.getInt("recommendedLevel") + : null + ); + + Logger.printDebug(() -> "Fetched: " + renderer); + + return renderer; + } catch (JSONException e) { + Logger.printException(() -> "Failed to get storyboardRenderer", e); + } + + return null; + } + + @Nullable + public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) { + Objects.requireNonNull(videoId); + + var renderer = getStoryboardRendererUsingBody(videoId, + String.format(ANDROID_INNER_TUBE_BODY, videoId), false); + if (renderer == null) { + Logger.printDebug(() -> videoId + " not available using Android client"); + renderer = getStoryboardRendererUsingBody(videoId, + String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId), true); + if (renderer == null) { + Logger.printDebug(() -> videoId + " not available using TV embedded client"); + } + } + + return renderer; + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java deleted file mode 100644 index 5cdfe56988..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java +++ /dev/null @@ -1,177 +0,0 @@ -package app.revanced.integrations.youtube.patches.spoof.requests; - -import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.GET_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.*; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; -import app.revanced.integrations.shared.settings.BaseSettings; -import app.revanced.integrations.youtube.patches.spoof.ClientType; - -public class StreamingDataRequest { - - private static final long MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; // 20 seconds - - private static final Map cache = Collections.synchronizedMap( - new LinkedHashMap<>(100) { - private static final int CACHE_LIMIT = 50; - - @Override - protected boolean removeEldestEntry(Entry eldest) { - return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit. - } - }); - - public static void fetchRequest(String videoId, Map fetchHeaders) { - // Always fetch, even if there is a existing request for the same video. - cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); - } - - @Nullable - public static StreamingDataRequest getRequestForVideoId(String videoId) { - return cache.get(videoId); - } - - private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) { - if (showToast) Utils.showToastShort(toastMessage); - Logger.printInfo(() -> toastMessage, ex); - } - - @Nullable - private static HttpURLConnection send(ClientType clientType, String videoId, - Map playerHeaders, - boolean showErrorToasts) { - Objects.requireNonNull(clientType); - Objects.requireNonNull(videoId); - Objects.requireNonNull(playerHeaders); - - final long startTime = System.currentTimeMillis(); - String clientTypeName = clientType.name(); - Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name()); - - try { - HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); - - String authHeader = playerHeaders.get("Authorization"); - String visitorId = playerHeaders.get("X-Goog-Visitor-Id"); - connection.setRequestProperty("Authorization", authHeader); - connection.setRequestProperty("X-Goog-Visitor-Id", visitorId); - - String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); - byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); - connection.setFixedLengthStreamingMode(requestBody.length); - connection.getOutputStream().write(requestBody); - - final int responseCode = connection.getResponseCode(); - if (responseCode == 200) return connection; - - handleConnectionError(clientTypeName + " not available with response code: " - + responseCode + " message: " + connection.getResponseMessage(), - null, showErrorToasts); - } catch (SocketTimeoutException ex) { - handleConnectionError("Connection timeout", ex, showErrorToasts); - } catch (IOException ex) { - handleConnectionError("Network error", ex, showErrorToasts); - } catch (Exception ex) { - Logger.printException(() -> "send failed", ex); - } finally { - Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms"); - } - - return null; - } - - private static ByteBuffer fetch(String videoId, Map playerHeaders) { - // Retry with different client if empty response body is received. - ClientType[] clientTypesToUse = { - ClientType.IOS, - ClientType.ANDROID_VR - }; - - final boolean debugEnabled = BaseSettings.DEBUG.get(); - if (debugEnabled) { - // To ensure the different clients are used while debugging, - // use a random client order. - Collections.shuffle(Arrays.asList(clientTypesToUse)); - } - - int i = 0; - for (ClientType clientType : clientTypesToUse) { - // Show an error if the last client type fails, or if the debug is enabled then show for all attempts. - final boolean showErrorToast = (++i == clientTypesToUse.length) || debugEnabled; - - 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) { - try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - byte[] buffer = new byte[8192]; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) >= 0) { - baos.write(buffer, 0, bytesRead); - } - - return ByteBuffer.wrap(baos.toByteArray()); - } - } - } - } catch (IOException ex) { - Logger.printException(() -> "Fetch failed while processing response data", ex); - } - } - } - - handleConnectionError("Could not fetch any client streams", null, debugEnabled); - return null; - } - - private final String videoId; - private final Future future; - - private StreamingDataRequest(String videoId, Map playerHeaders) { - Objects.requireNonNull(playerHeaders); - this.videoId = videoId; - this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders)); - } - - @Nullable - public ByteBuffer getStream() { - try { - return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS); - } catch (TimeoutException ex) { - Logger.printInfo(() -> "getStream timed out", ex); - } catch (InterruptedException ex) { - Logger.printException(() -> "getStream interrupted", ex); - Thread.currentThread().interrupt(); // Restore interrupt status flag. - } catch (ExecutionException ex) { - Logger.printException(() -> "getStream failure", ex); - } - - return null; - } - - @NonNull - @Override - public String toString() { - return "StreamingDataRequest{" + "videoId='" + videoId + '\'' + '}'; - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 9571b36ebb..9794f48973 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -8,6 +8,7 @@ import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.ThumbnailOption; import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.ThumbnailStillTime; import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch; +import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch; import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings; import java.util.Arrays; @@ -18,6 +19,7 @@ import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerHideExpandCloseAvailability; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*; +import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; @@ -258,8 +260,9 @@ public class Settings extends BaseSettings { public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE); public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE); public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true,"revanced_spoof_client_user_dialog_message"); - public static final BooleanSetting SPOOF_CLIENT_FORCE_AVC = new BooleanSetting("revanced_spoof_client_force_avc", FALSE, true, - "revanced_spoof_client_force_avc_user_dialog_message", parent(SPOOF_CLIENT)); + public static final BooleanSetting SPOOF_CLIENT_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_client_ios_force_avc", FALSE, true, + "revanced_spoof_client_ios_force_avc_user_dialog_message", new SpoofClientPatch.ForceiOSAVCAvailability()); + public static final EnumSetting SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", ClientType.IOS, true, parent(SPOOF_CLIENT)); @Deprecated public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", ""); public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1); diff --git a/stub/src/main/java/org/chromium/net/ExperimentalUrlRequest.java b/stub/src/main/java/org/chromium/net/ExperimentalUrlRequest.java new file mode 100644 index 0000000000..cdf2593e79 --- /dev/null +++ b/stub/src/main/java/org/chromium/net/ExperimentalUrlRequest.java @@ -0,0 +1,8 @@ +package org.chromium.net; + +public abstract class ExperimentalUrlRequest { + public abstract class Builder { + public abstract ExperimentalUrlRequest.Builder addHeader(String name, String value); + public abstract ExperimentalUrlRequest build(); + } +} diff --git a/stub/src/main/java/org/chromium/net/UrlRequest.java b/stub/src/main/java/org/chromium/net/UrlRequest.java index 4c02f1a400..565fc22274 100644 --- a/stub/src/main/java/org/chromium/net/UrlRequest.java +++ b/stub/src/main/java/org/chromium/net/UrlRequest.java @@ -1,8 +1,4 @@ package org.chromium.net; public abstract class UrlRequest { - public abstract class Builder { - public abstract Builder addHeader(String name, String value); - public abstract UrlRequest build(); - } } From cb72fa86e48e8587cfaa7032a40bdd3376f73f24 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:01:31 -0400 Subject: [PATCH 51/91] fix(Change start page): Add limited support for 19.25+ --- .../youtube/patches/ChangeStartPagePatch.java | 49 ++++++++++++++++--- .../youtube/settings/Settings.java | 2 +- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java index f31af61b1b..b5d5170acb 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java @@ -2,20 +2,53 @@ import android.content.Intent; import android.net.Uri; + import app.revanced.integrations.shared.Logger; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") public final class ChangeStartPagePatch { - public static void changeIntent(final Intent intent) { - final var startPage = Settings.START_PAGE.get(); - if (startPage.isEmpty()) return; + private static final String MAIN_ACTIONS = "android.intent.action.MAIN"; + + /** + * Injection point. + */ + public static void changeStartPageLegacy(Intent intent) { + changeStartPage(intent, true); + } + + /** + * Injection point. + */ + public static void changeStartPage(Intent intent) { + changeStartPage(intent, false); + } + + private static void changeStartPage(Intent intent, boolean useUrlStartPages) { + try { + Logger.printDebug(() -> "action: " + intent.getAction() + " data: " + intent.getData() + " extras: " + intent.getExtras()); + + if (!MAIN_ACTIONS.equals(intent.getAction())) { + return; + } + + final var startPage = Settings.START_PAGE.get(); + if (startPage.isEmpty()) return; + + if (startPage.startsWith("open.")) { + intent.setAction("com.google.android.youtube.action." + startPage); - Logger.printDebug(() -> "Changing start page to " + startPage); + Logger.printDebug(() -> "Changed start page shortcut to: " + startPage); + } else if (useUrlStartPages && startPage.startsWith("www.youtube.com/")) { + intent.setData(Uri.parse(startPage)); - if (startPage.startsWith("www")) - intent.setData(Uri.parse(startPage)); - else - intent.setAction("com.google.android.youtube.action." + startPage); + Logger.printDebug(() -> "Changed start page url to: " + startPage); + } else { + Logger.printDebug(() -> "Cannot change start page to: " + startPage); + Settings.START_PAGE.resetToDefault(); + } + } catch (Exception ex) { + Logger.printException(() -> "changeIntent failure", ex); + } } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 9794f48973..3556b308e4 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -191,7 +191,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_VIDEO_QUALITY_MENU_FOOTER = new BooleanSetting("revanced_hide_video_quality_menu_footer", FALSE); // General layout - public static final StringSetting START_PAGE = new StringSetting("revanced_start_page", ""); + public static final StringSetting START_PAGE = new StringSetting("revanced_start_page", "", true); public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", FALSE, true, "revanced_spoof_app_version_user_dialog_message"); public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "17.33.42", true, parent(SPOOF_APP_VERSION)); public static final BooleanSetting TABLET_LAYOUT = new BooleanSetting("revanced_tablet_layout", FALSE, true, "revanced_tablet_layout_user_dialog_message"); From 93eac6989e0465917734fa68405b504f7e626977 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 16 Sep 2024 23:51:24 -0400 Subject: [PATCH 52/91] Comments --- .../integrations/youtube/patches/MiniplayerPatch.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java index fa3d4ccf89..ad84ddb60e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java @@ -65,8 +65,7 @@ public boolean isModern() { DisplayMetrics displayMetrics = Utils.getContext().getResources().getDisplayMetrics(); final int deviceDipWidth = (int) (displayMetrics.widthPixels / displayMetrics.density); - // YT seems to use a minimum height to calculate - // the minimum miniplayer width based on the video. + // YT seems to use a minimum height to calculate the minimum miniplayer width based on the video. // 170 seems to be the smallest that can be used and using less makes no difference. final int WIDTH_DIP_MIN = 170; // Seems to be the smallest that works. final int HORIZONTAL_PADDING_DIP = 15; // Estimated padding. @@ -109,7 +108,7 @@ public boolean isModern() { private static final boolean DOUBLE_TAP_ACTION_ENABLED = // 19.29+ is very broken if double tap is not enabled. IS_19_29_OR_GREATER || - (CURRENT_TYPE.isModern() & Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get()); + (CURRENT_TYPE.isModern() && Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get()); private static final boolean DRAG_AND_DROP_ENABLED = CURRENT_TYPE.isModern() && Settings.MINIPLAYER_DRAG_AND_DROP.get(); From 8fbf23a457c1638390a8ca27087bd1bbe8d46e60 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 17 Sep 2024 00:40:26 -0400 Subject: [PATCH 53/91] fix: Changing default of 'hide shorts navigation bar' default to off, as this seems to have odd issues when resuming the app --- .../app/revanced/integrations/youtube/settings/Settings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 3556b308e4..44f3d45c0c 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -238,7 +238,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_SHORTS_VIDEO_TITLE = new BooleanSetting("revanced_hide_shorts_video_title", FALSE); public static final BooleanSetting HIDE_SHORTS_SOUND_METADATA_LABEL = new BooleanSetting("revanced_hide_shorts_sound_metadata_label", FALSE); public static final BooleanSetting HIDE_SHORTS_FULL_VIDEO_LINK_LABEL = new BooleanSetting("revanced_hide_shorts_full_video_link_label", FALSE); - public static final BooleanSetting HIDE_SHORTS_NAVIGATION_BAR = new BooleanSetting("revanced_hide_shorts_navigation_bar", TRUE, true); + public static final BooleanSetting HIDE_SHORTS_NAVIGATION_BAR = new BooleanSetting("revanced_hide_shorts_navigation_bar", FALSE, true); // Seekbar public static final BooleanSetting DISABLE_PRECISE_SEEKING_GESTURE = new BooleanSetting("revanced_disable_precise_seeking_gesture", TRUE); From bf73f8043b383154caf3d48a1bc4e1b016dec04c Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:16:19 -0400 Subject: [PATCH 54/91] fix(Enable slide to seek): Require restart --- .../integrations/youtube/patches/SlideToSeekPatch.java | 4 +++- .../app/revanced/integrations/youtube/settings/Settings.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java index 5a6f56290c..1d5d427bb2 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java @@ -4,7 +4,9 @@ @SuppressWarnings("unused") public final class SlideToSeekPatch { + private static final Boolean SLIDE_TO_SEEK_DISABLED = !Settings.SLIDE_TO_SEEK.get(); + public static boolean isSlideToSeekDisabled() { - return !Settings.SLIDE_TO_SEEK.get(); + return SLIDE_TO_SEEK_DISABLED; } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 44f3d45c0c..0662d3987a 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -243,7 +243,7 @@ public class Settings extends BaseSettings { // Seekbar public static final BooleanSetting DISABLE_PRECISE_SEEKING_GESTURE = new BooleanSetting("revanced_disable_precise_seeking_gesture", TRUE); public static final BooleanSetting SEEKBAR_TAPPING = new BooleanSetting("revanced_seekbar_tapping", TRUE); - public static final BooleanSetting SLIDE_TO_SEEK = new BooleanSetting("revanced_slide_to_seek", FALSE); + public static final BooleanSetting SLIDE_TO_SEEK = new BooleanSetting("revanced_slide_to_seek", FALSE, true); public static final BooleanSetting RESTORE_OLD_SEEKBAR_THUMBNAILS = new BooleanSetting("revanced_restore_old_seekbar_thumbnails", TRUE); public static final BooleanSetting HIDE_SEEKBAR = new BooleanSetting("revanced_hide_seekbar", FALSE, true); public static final BooleanSetting HIDE_SEEKBAR_THUMBNAIL = new BooleanSetting("revanced_hide_seekbar_thumbnail", FALSE); From 0648478de88beca53c5fecb6b2fab518defb4907 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:15:37 -0400 Subject: [PATCH 55/91] fix: Show an error if a preference has a key but no setting --- .../settings/preference/AbstractPreferenceFragment.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java index b7fa68ac69..e6d07ccde1 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java @@ -11,6 +11,7 @@ import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.shared.settings.BaseSettings; import app.revanced.integrations.shared.settings.BooleanSetting; import app.revanced.integrations.shared.settings.Setting; @@ -141,8 +142,13 @@ private void updatePreferenceScreen(@NonNull PreferenceScreen screen, } else if (pref.hasKey()) { String key = pref.getKey(); Setting setting = Setting.getSettingFromPath(key); + if (setting != null) { updatePreference(pref, setting, syncSettingValue, applySettingToPreference); + } else if (BaseSettings.DEBUG.get() && (pref instanceof SwitchPreference + || pref instanceof EditTextPreference || pref instanceof ListPreference)) { + // Probably a typo in the patches preference declaration. + Logger.printException(() -> "Preference key has no setting: " + key); } } } From 89ba56a6dabfc4282e72597840c1956f2d968d65 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:33:02 -0400 Subject: [PATCH 56/91] fix(Change start page): Fix url start pages Code courtesy of: https://github.com/inotia00/revanced-integrations/blob/420c159d9620eefb776b3e2c5b53ad035200dc46/app/src/main/java/app/revanced/integrations/youtube/patches/general/ChangeStartPagePatch.java --- .../youtube/patches/ChangeStartPagePatch.java | 135 ++++++++++++++---- .../youtube/settings/Settings.java | 3 +- 2 files changed, 107 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java index b5d5170acb..d87dca10d3 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java @@ -1,54 +1,129 @@ package app.revanced.integrations.youtube.patches; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + import android.content.Intent; -import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") public final class ChangeStartPagePatch { - private static final String MAIN_ACTIONS = "android.intent.action.MAIN"; + + public enum StartPage { + /** + * Unmodified type, and same as un-patched. + */ + ORIGINAL("", null), + + /** + * Browse id. + */ + BROWSE("FEguide_builder", TRUE), + EXPLORE("FEexplore", TRUE), + HISTORY("FEhistory", TRUE), + LIBRARY("FElibrary", TRUE), + MOVIE("FEstorefront", TRUE), + SUBSCRIPTIONS("FEsubscriptions", TRUE), + TRENDING("FEtrending", TRUE), + + /** + * Channel id, this can be used as a browseId. + */ + GAMING("UCOpNcN46UbXVtpKMrmU4Abg", TRUE), + LIVE("UC4R8DWoMoI7CAwX8_LjQHig", TRUE), + MUSIC("UC-9-kyTW8ZkZNDHQJ6FgpwQ", TRUE), + SPORTS("UCEgdi0XIXXZ-qJOFPf4JSKw", TRUE), + + /** + * Playlist id, this can be used as a browseId. + */ + LIKED_VIDEO("VLLL", TRUE), + WATCH_LATER("VLWL", TRUE), + + /** + * Intent action. + */ + SEARCH("com.google.android.youtube.action.open.search", FALSE), + SHORTS("com.google.android.youtube.action.open.shorts", FALSE); + + @Nullable + final Boolean isBrowseId; + + @NonNull + final String id; + + StartPage(@NonNull String id, @Nullable Boolean isBrowseId) { + this.id = id; + this.isBrowseId = isBrowseId; + } + + private boolean isBrowseId() { + return TRUE.equals(isBrowseId); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean isIntentAction() { + return FALSE.equals(isBrowseId); + } + } /** - * Injection point. + * Intent action when YouTube is cold started from the launcher. + *

+ * If you don't check this, the hooking will also apply in the following cases: + * Case 1. The user clicked Shorts button on the YouTube shortcut. + * Case 2. The user clicked Shorts button on the YouTube widget. + * In this case, instead of opening Shorts, the start page specified by the user is opened. */ - public static void changeStartPageLegacy(Intent intent) { - changeStartPage(intent, true); - } + private static final String ACTION_MAIN = "android.intent.action.MAIN"; + + private static final StartPage START_PAGE = Settings.CHANGE_START_PAGE.get(); /** - * Injection point. + * There is an issue where the back button on the toolbar doesn't work properly. + * As a workaround for this issue, instead of overriding the browserId multiple times, just override it once. */ - public static void changeStartPage(Intent intent) { - changeStartPage(intent, false); - } + private static boolean appLaunched = false; - private static void changeStartPage(Intent intent, boolean useUrlStartPages) { - try { - Logger.printDebug(() -> "action: " + intent.getAction() + " data: " + intent.getData() + " extras: " + intent.getExtras()); + public static String overrideBrowseId(@NonNull String original) { + if (!START_PAGE.isBrowseId()) { + return original; + } - if (!MAIN_ACTIONS.equals(intent.getAction())) { - return; - } + if (appLaunched) { + Logger.printDebug(() -> "Ignore override browseId as the app already launched"); + return original; + } + appLaunched = true; - final var startPage = Settings.START_PAGE.get(); - if (startPage.isEmpty()) return; + Logger.printDebug(() -> "Changing browseId to " + START_PAGE.id); + return START_PAGE.id; + } - if (startPage.startsWith("open.")) { - intent.setAction("com.google.android.youtube.action." + startPage); + public static void overrideIntentAction(@NonNull Intent intent) { + if (!START_PAGE.isIntentAction()) { + return; + } - Logger.printDebug(() -> "Changed start page shortcut to: " + startPage); - } else if (useUrlStartPages && startPage.startsWith("www.youtube.com/")) { - intent.setData(Uri.parse(startPage)); + if (!ACTION_MAIN.equals(intent.getAction())) { + Logger.printDebug(() -> "Ignore override intent action" + + " as the current activity is not the entry point of the application"); + return; + } - Logger.printDebug(() -> "Changed start page url to: " + startPage); - } else { - Logger.printDebug(() -> "Cannot change start page to: " + startPage); - Settings.START_PAGE.resetToDefault(); - } - } catch (Exception ex) { - Logger.printException(() -> "changeIntent failure", ex); + if (appLaunched) { + Logger.printDebug(() -> "Ignore override intent action as the app already launched"); + return; } + appLaunched = true; + + final String intentAction = START_PAGE.id; + Logger.printDebug(() -> "Changing intent action to " + intentAction); + intent.setAction(intentAction); } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 2e465fbd73..5ac478ed7c 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -17,6 +17,7 @@ import java.util.Set; import static app.revanced.integrations.shared.settings.Setting.*; +import static app.revanced.integrations.youtube.patches.ChangeStartPagePatch.StartPage; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerHideExpandCloseAvailability; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*; @@ -191,7 +192,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_VIDEO_QUALITY_MENU_FOOTER = new BooleanSetting("revanced_hide_video_quality_menu_footer", FALSE); // General layout - public static final StringSetting START_PAGE = new StringSetting("revanced_start_page", "", true); + public static final EnumSetting CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.ORIGINAL, true); public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", FALSE, true, "revanced_spoof_app_version_user_dialog_message"); public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "17.33.42", true, parent(SPOOF_APP_VERSION)); public static final BooleanSetting TABLET_LAYOUT = new BooleanSetting("revanced_tablet_layout", FALSE, true, "revanced_tablet_layout_user_dialog_message"); From 32b189e5cea2273d43f5b0e1ba60621e45d07cc7 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:38:56 -0400 Subject: [PATCH 57/91] fix(Hide Shorts components): Use same `Hide Shorts Navigation bar` setting until app is restarted --- .../youtube/patches/components/ShortsFilter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java index a7935f71c3..238cbc2a92 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java @@ -16,6 +16,7 @@ @SuppressWarnings("unused") public final class ShortsFilter extends Filter { + public static final Boolean HIDE_SHORTS_NAVIGATION_BAR = Settings.HIDE_SHORTS_NAVIGATION_BAR.get(); public static PivotBar pivotBar; // Set by patch. private final static String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml"; @@ -351,14 +352,14 @@ public static void hideShortsShareButton(final View shareButtonView) { // endregion public static void hideNavigationBar() { - if (!Settings.HIDE_SHORTS_NAVIGATION_BAR.get()) return; + if (!HIDE_SHORTS_NAVIGATION_BAR) return; if (pivotBar == null) return; pivotBar.setVisibility(View.GONE); } public static View hideNavigationBar(final View navigationBarView) { - if (Settings.HIDE_SHORTS_NAVIGATION_BAR.get()) + if (HIDE_SHORTS_NAVIGATION_BAR) return null; // Hides the navigation bar. return navigationBarView; From c2a25047fd05d5b701dc8b12949d7374469b264a Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:29:28 -0400 Subject: [PATCH 58/91] fix(Hide Shorts components): Simplify fingerprints, use weak reference for navigation bar --- .../youtube/patches/components/ShortsFilter.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java index 238cbc2a92..ee92adc366 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java @@ -9,6 +9,8 @@ import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar; +import java.lang.ref.WeakReference; + import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.youtube.shared.NavigationBar; @@ -17,14 +19,14 @@ @SuppressWarnings("unused") public final class ShortsFilter extends Filter { public static final Boolean HIDE_SHORTS_NAVIGATION_BAR = Settings.HIDE_SHORTS_NAVIGATION_BAR.get(); - public static PivotBar pivotBar; // Set by patch. - private final static String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml"; /** * For paid promotion label and subscribe button that appears in the channel bar. */ private final static String REEL_METAPANEL_PATH = "reel_metapanel.eml"; + private static WeakReference pivotBarRef = new WeakReference<>(null); + private final StringFilterGroup shortsCompactFeedVideoPath; private final ByteArrayFilterGroup shortsCompactFeedVideoBuffer; @@ -351,8 +353,13 @@ public static void hideShortsShareButton(final View shareButtonView) { // endregion + public static void setNavigationBar(PivotBar view) { + pivotBarRef = new WeakReference<>(view); + } + public static void hideNavigationBar() { if (!HIDE_SHORTS_NAVIGATION_BAR) return; + var pivotBar = pivotBarRef.get(); if (pivotBar == null) return; pivotBar.setVisibility(View.GONE); From 1515a908c1c0e55692ef2db151f99c4aa3bc068f Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 18 Sep 2024 20:35:14 -0400 Subject: [PATCH 59/91] fix(Hide Shorts components): Fix nav bar hiding at wrong times --- .../patches/components/ShortsFilter.java | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java index ee92adc366..bfba4eec83 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java @@ -11,6 +11,7 @@ import java.lang.ref.WeakReference; +import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.youtube.shared.NavigationBar; @@ -24,6 +25,10 @@ public final class ShortsFilter extends Filter { * For paid promotion label and subscribe button that appears in the channel bar. */ private final static String REEL_METAPANEL_PATH = "reel_metapanel.eml"; + /** + * Tag that appears when opening the Shorts player. + */ + private static final String REEL_WATCH_FRAGMENT_INIT_PLAYBACK = "r_fs"; private static WeakReference pivotBarRef = new WeakReference<>(null); @@ -220,9 +225,7 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) { // Selectively filter to avoid false positive filtering of other subscribe/join buttons. if (path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH)) { - return super.isFiltered( - identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex - ); + return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); } return false; } @@ -237,9 +240,7 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff // Video action buttons (like, dislike, comment, share, remix) have the same path. if (matchedGroup == actionBar) { if (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) { - return super.isFiltered( - identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex - ); + return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); } return false; } @@ -247,9 +248,7 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff if (matchedGroup == suggestedAction) { // Suggested actions can be at the start or in the middle of a path. if (suggestedActionsGroupList.check(protobufBufferArray).isFiltered()) { - return super.isFiltered( - identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex - ); + return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); } return false; } @@ -354,20 +353,29 @@ public static void hideShortsShareButton(final View shareButtonView) { // endregion public static void setNavigationBar(PivotBar view) { + Logger.printDebug(() -> "Setting navigation bar"); pivotBarRef = new WeakReference<>(view); } - public static void hideNavigationBar() { - if (!HIDE_SHORTS_NAVIGATION_BAR) return; - var pivotBar = pivotBarRef.get(); - if (pivotBar == null) return; + public static void hideNavigationBar(String tag) { + if (HIDE_SHORTS_NAVIGATION_BAR) { + if (tag.equals(REEL_WATCH_FRAGMENT_INIT_PLAYBACK)) { + var pivotBar = pivotBarRef.get(); + if (pivotBar == null) return; - pivotBar.setVisibility(View.GONE); + Logger.printDebug(() -> "Hiding navbar by setting to GONE"); + pivotBar.setVisibility(View.GONE); + } else { + Logger.printDebug(() -> "Ignoring tag: " + tag); + } + } } public static View hideNavigationBar(final View navigationBarView) { - if (HIDE_SHORTS_NAVIGATION_BAR) + if (HIDE_SHORTS_NAVIGATION_BAR) { + Logger.printDebug(() -> "Hiding navbar by returning null"); return null; // Hides the navigation bar. + } return navigationBarView; } From f63a3987f80d51db8f3d9347d306aeb7f8de62c5 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 18 Sep 2024 21:18:45 -0400 Subject: [PATCH 60/91] fix(Hide Shorts components): Do not cut off Shorts seekbar if navbar is hidden --- .../patches/components/ShortsFilter.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java index bfba4eec83..7384f629ae 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java @@ -4,6 +4,7 @@ import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; import android.view.View; +import android.view.ViewGroup; import androidx.annotation.Nullable; @@ -30,6 +31,13 @@ public final class ShortsFilter extends Filter { */ private static final String REEL_WATCH_FRAGMENT_INIT_PLAYBACK = "r_fs"; + /** + * When the navbar is hidden, the amount of height to use where the bottom container went. + * If set to zero, then the Shorts seekbar will at the very bottom of the screen + * and the seekbar handle is half cut off. + */ + private static final int REEL_WATCH_HIDDEN_NAVBAR_BOTTOM_CONTAINER_HEIGHT = 50; + private static WeakReference pivotBarRef = new WeakReference<>(null); private final StringFilterGroup shortsCompactFeedVideoPath; @@ -359,7 +367,7 @@ public static void setNavigationBar(PivotBar view) { public static void hideNavigationBar(String tag) { if (HIDE_SHORTS_NAVIGATION_BAR) { - if (tag.equals(REEL_WATCH_FRAGMENT_INIT_PLAYBACK)) { + if (REEL_WATCH_FRAGMENT_INIT_PLAYBACK.equals(tag)) { var pivotBar = pivotBarRef.get(); if (pivotBar == null) return; @@ -371,12 +379,11 @@ public static void hideNavigationBar(String tag) { } } - public static View hideNavigationBar(final View navigationBarView) { + public static void setBottomBarContainerSize(View bottomBarContainer) { if (HIDE_SHORTS_NAVIGATION_BAR) { - Logger.printDebug(() -> "Hiding navbar by returning null"); - return null; // Hides the navigation bar. + ViewGroup.LayoutParams params = bottomBarContainer.getLayoutParams(); + params.height = REEL_WATCH_HIDDEN_NAVBAR_BOTTOM_CONTAINER_HEIGHT; + bottomBarContainer.setLayoutParams(params); } - - return navigationBarView; } } From 0acd61bea3716bbc877ff2874b2b389d74b8dce9 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 18 Sep 2024 21:27:18 -0400 Subject: [PATCH 61/91] revert fix(Hide Shorts components). Cannot set a height otherwise if comment button is pressed the navbar becomes permanently too short. --- .../youtube/patches/components/ShortsFilter.java | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java index 7384f629ae..7d6fb1e9d2 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java @@ -4,7 +4,6 @@ import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; import android.view.View; -import android.view.ViewGroup; import androidx.annotation.Nullable; @@ -31,13 +30,6 @@ public final class ShortsFilter extends Filter { */ private static final String REEL_WATCH_FRAGMENT_INIT_PLAYBACK = "r_fs"; - /** - * When the navbar is hidden, the amount of height to use where the bottom container went. - * If set to zero, then the Shorts seekbar will at the very bottom of the screen - * and the seekbar handle is half cut off. - */ - private static final int REEL_WATCH_HIDDEN_NAVBAR_BOTTOM_CONTAINER_HEIGHT = 50; - private static WeakReference pivotBarRef = new WeakReference<>(null); private final StringFilterGroup shortsCompactFeedVideoPath; @@ -379,11 +371,11 @@ public static void hideNavigationBar(String tag) { } } - public static void setBottomBarContainerSize(View bottomBarContainer) { + public static View hideNavigationBar(View bottomBarContainer) { if (HIDE_SHORTS_NAVIGATION_BAR) { - ViewGroup.LayoutParams params = bottomBarContainer.getLayoutParams(); - params.height = REEL_WATCH_HIDDEN_NAVBAR_BOTTOM_CONTAINER_HEIGHT; - bottomBarContainer.setLayoutParams(params); + return null; } + + return bottomBarContainer; } } From 06478290bafab2add9fc7a7f7b30618a081526fa Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:10:30 -0400 Subject: [PATCH 62/91] fix(Hide Shorts components): If hiding the the nav bar, then use padding between seekbar and bottom of screen --- .../youtube/patches/components/ShortsFilter.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java index 7d6fb1e9d2..54b75290bd 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java @@ -21,15 +21,22 @@ public final class ShortsFilter extends Filter { public static final Boolean HIDE_SHORTS_NAVIGATION_BAR = Settings.HIDE_SHORTS_NAVIGATION_BAR.get(); private final static String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml"; + /** * For paid promotion label and subscribe button that appears in the channel bar. */ private final static String REEL_METAPANEL_PATH = "reel_metapanel.eml"; + /** * Tag that appears when opening the Shorts player. */ private static final String REEL_WATCH_FRAGMENT_INIT_PLAYBACK = "r_fs"; + /** + * Vertical padding between the bottom of the screen and the seekbar, when the Shorts navigation bar is hidden. + */ + public static final int HIDDEN_NAVIGATION_BAR_VERTICAL_HEIGHT = 70; + private static WeakReference pivotBarRef = new WeakReference<>(null); private final StringFilterGroup shortsCompactFeedVideoPath; @@ -371,11 +378,11 @@ public static void hideNavigationBar(String tag) { } } - public static View hideNavigationBar(View bottomBarContainer) { + public static int getNavigationBarHeight(int original) { if (HIDE_SHORTS_NAVIGATION_BAR) { - return null; + return HIDDEN_NAVIGATION_BAR_VERTICAL_HEIGHT; } - return bottomBarContainer; + return original; } } From d118809463331fa9ecc03deb112faffceaa4fa8e Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:48:00 -0400 Subject: [PATCH 63/91] fix(Hide Shorts components): Also hide empty space if sound button is hidden --- .../youtube/patches/components/ShortsFilter.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java index 54b75290bd..6df2c09245 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java @@ -328,6 +328,14 @@ public static void hideShortsShelf(final View shortsShelfView) { } } + public static int getSoundButtonSize(int original) { + if (Settings.HIDE_SHORTS_SOUND_BUTTON.get()) { + return 0; + } + + return original; + } + // region Hide the buttons in older versions of YouTube. New versions use Litho. public static void hideLikeButton(final View likeButtonView) { From 4a57d8baab2904623e085f81f9501c72bcd3bd44 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:00:20 -0400 Subject: [PATCH 64/91] fix(Hide Shorts components): Hide navigation bar when opening a Short suggestion below a video --- .../youtube/patches/components/ShortsFilter.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java index 6df2c09245..278716dc8e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java @@ -10,6 +10,8 @@ import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar; import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.List; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; @@ -28,9 +30,9 @@ public final class ShortsFilter extends Filter { private final static String REEL_METAPANEL_PATH = "reel_metapanel.eml"; /** - * Tag that appears when opening the Shorts player. + * Tags that appears when opening the Shorts player. */ - private static final String REEL_WATCH_FRAGMENT_INIT_PLAYBACK = "r_fs"; + private static final List REEL_WATCH_FRAGMENT_INIT_PLAYBACK = Arrays.asList("r_fs", "r_ts"); /** * Vertical padding between the bottom of the screen and the seekbar, when the Shorts navigation bar is hidden. @@ -374,7 +376,7 @@ public static void setNavigationBar(PivotBar view) { public static void hideNavigationBar(String tag) { if (HIDE_SHORTS_NAVIGATION_BAR) { - if (REEL_WATCH_FRAGMENT_INIT_PLAYBACK.equals(tag)) { + if (REEL_WATCH_FRAGMENT_INIT_PLAYBACK.contains(tag)) { var pivotBar = pivotBarRef.get(); if (pivotBar == null) return; From fffe3048b9f4bddfc31db101d3c0d15f330e95d1 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:35:24 -0400 Subject: [PATCH 65/91] fix(Miniplayer): Fix broken scrolling if 'default' is used --- .../youtube/patches/MiniplayerPatch.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java index ad84ddb60e..8336a8da2a 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java @@ -199,8 +199,11 @@ public static void adjustMiniplayerOpacity(ImageView view) { public static boolean getModernFeatureFlagsActiveOverride(boolean original) { Logger.printDebug(() -> "getModernFeatureFlagsActiveOverride original: " + original); - // This should always be true if either double tap or drag and drop are enabled. - return true; + if (CURRENT_TYPE == ORIGINAL) { + return original; + } + + return CURRENT_TYPE.isModern(); } /** @@ -209,6 +212,10 @@ public static boolean getModernFeatureFlagsActiveOverride(boolean original) { public static boolean enableMiniplayerDoubleTapAction(boolean original) { Logger.printDebug(() -> "enableMiniplayerDoubleTapAction original: " + original); + if (CURRENT_TYPE == ORIGINAL) { + return original; + } + return DOUBLE_TAP_ACTION_ENABLED; } @@ -218,6 +225,10 @@ public static boolean enableMiniplayerDoubleTapAction(boolean original) { public static boolean enableMiniplayerDragAndDrop(boolean original) { Logger.printDebug(() -> "enableMiniplayerDragAndDrop original: " + original); + if (CURRENT_TYPE == ORIGINAL) { + return original; + } + return DRAG_AND_DROP_ENABLED; } From d85ca8704e3542581d83fd13a2dfcca8d410a5dd Mon Sep 17 00:00:00 2001 From: zainarbani Date: Sun, 22 Sep 2024 15:42:09 +0700 Subject: [PATCH 66/91] fix: Resolve `slide to seek` remaining issue --- .../integrations/youtube/patches/SlideToSeekPatch.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java index 1d5d427bb2..7d6b209012 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java @@ -6,7 +6,9 @@ public final class SlideToSeekPatch { private static final Boolean SLIDE_TO_SEEK_DISABLED = !Settings.SLIDE_TO_SEEK.get(); - public static boolean isSlideToSeekDisabled() { + public static boolean isSlideToSeekDisabled(boolean isDisabled) { + if (!isDisabled) return isDisabled; + return SLIDE_TO_SEEK_DISABLED; } } From 460534a34e24830fcce223fcd8ef87f77fc921fd Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 24 Sep 2024 02:17:48 -0400 Subject: [PATCH 67/91] fix: Only log miniplayer feature flags if they were originally turned on --- .../youtube/patches/MiniplayerPatch.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java index 8336a8da2a..938358f8d9 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java @@ -197,7 +197,7 @@ public static void adjustMiniplayerOpacity(ImageView view) { * Injection point. */ public static boolean getModernFeatureFlagsActiveOverride(boolean original) { - Logger.printDebug(() -> "getModernFeatureFlagsActiveOverride original: " + original); + if (original) Logger.printDebug(() -> "getModernFeatureFlagsActiveOverride original: " + original); if (CURRENT_TYPE == ORIGINAL) { return original; @@ -210,7 +210,7 @@ public static boolean getModernFeatureFlagsActiveOverride(boolean original) { * Injection point. */ public static boolean enableMiniplayerDoubleTapAction(boolean original) { - Logger.printDebug(() -> "enableMiniplayerDoubleTapAction original: " + original); + if (original) Logger.printDebug(() -> "enableMiniplayerDoubleTapAction original: " + true); if (CURRENT_TYPE == ORIGINAL) { return original; @@ -223,7 +223,7 @@ public static boolean enableMiniplayerDoubleTapAction(boolean original) { * Injection point. */ public static boolean enableMiniplayerDragAndDrop(boolean original) { - Logger.printDebug(() -> "enableMiniplayerDragAndDrop original: " + original); + if (original) Logger.printDebug(() -> "enableMiniplayerDragAndDrop original: " + true); if (CURRENT_TYPE == ORIGINAL) { return original; @@ -249,7 +249,7 @@ public static int setMiniplayerDefaultSize(int original) { public static float setMovementBoundFactor(float original) { // Not clear if this is useful to customize or not. // So for now just log this and keep whatever is the original value. - Logger.printDebug(() -> "setMovementBoundFactor original: " + original); + if (original != 1.0) Logger.printDebug(() -> "setMovementBoundFactor original: " + original); return original; } @@ -258,7 +258,7 @@ public static float setMovementBoundFactor(float original) { * Injection point. */ public static boolean setDropShadow(boolean original) { - Logger.printDebug(() -> "setViewElevation original: " + original); + if (original) Logger.printDebug(() -> "setViewElevation original: " + true); return original; } @@ -267,7 +267,7 @@ public static boolean setDropShadow(boolean original) { * Injection point. */ public static boolean setUseBackgroundViewOutlineProvider(boolean original) { - Logger.printDebug(() -> "setUseBackgroundViewOutlineProvider original: " + original); + if (original) Logger.printDebug(() -> "setUseBackgroundViewOutlineProvider original: " + true); return original; } From 954b82a161f1631dc7bb336278555d0716db8951 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 24 Sep 2024 02:29:22 -0400 Subject: [PATCH 68/91] fix: Increase bottom padding if Shorts navbar is hidden. This feature is still somewhat inconsistent --- .../integrations/youtube/patches/components/ShortsFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java index 278716dc8e..97e8a09d9a 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java @@ -37,7 +37,7 @@ public final class ShortsFilter extends Filter { /** * Vertical padding between the bottom of the screen and the seekbar, when the Shorts navigation bar is hidden. */ - public static final int HIDDEN_NAVIGATION_BAR_VERTICAL_HEIGHT = 70; + public static final int HIDDEN_NAVIGATION_BAR_VERTICAL_HEIGHT = 100; private static WeakReference pivotBarRef = new WeakReference<>(null); From e396c50e2486a21414856218605df9452263662f Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 27 Sep 2024 18:17:33 -0400 Subject: [PATCH 69/91] fix(Settings): Do not show background color in edit text preference copy/paste popup Code adapted from https://github.com/inotia00/revanced-integrations/commit/9e4ad2a18bf3247baef066b514dbe7eaaed8111f --- .../revanced/integrations/shared/Utils.java | 25 ++++++++++++++++--- .../preference/ImportExportPreference.java | 2 ++ .../ResettableEditTextPreference.java | 4 +++ .../SponsorBlockPreferenceFragment.java | 2 ++ .../SegmentCategoryListPreference.java | 3 +++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/shared/Utils.java b/app/src/main/java/app/revanced/integrations/shared/Utils.java index 0284447b9e..2760547ee4 100644 --- a/app/src/main/java/app/revanced/integrations/shared/Utils.java +++ b/app/src/main/java/app/revanced/integrations/shared/Utils.java @@ -1,10 +1,7 @@ package app.revanced.integrations.shared; import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; +import android.app.*; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; @@ -724,4 +721,24 @@ public static void sortPreferenceGroups(@NonNull PreferenceGroup group) { pref.setOrder(order); } } + + /** + * If {@link Fragment} uses [Android library] rather than [AndroidX library], + * the Dialog theme corresponding to [Android library] should be used. + *

+ * If not, the following issues will occur: + * ReVanced/revanced-patches#3061 + *

+ * To prevent these issues, apply the Dialog theme corresponding to [Android library]. + * + * @param builder Alertdialog builder to apply theme to. + * When used in a method containing an override, it must be called before 'super'. + */ + public static void setEditTextDialogTheme(AlertDialog.Builder builder) { + final int editTextDialogStyle = getResourceIdentifier( + "revanced_edit_text_dialog_style", "style"); + if (editTextDialogStyle != 0) { + builder.getContext().setTheme(editTextDialogStyle); + } + } } diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java index 5c8e7c9ba9..1b86c3cde2 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java @@ -66,6 +66,8 @@ public boolean onPreferenceClick(Preference preference) { @Override protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { try { + Utils.setEditTextDialogTheme(builder); + // Show the user the settings in JSON format. builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> { Utils.setClipboard(getEditText().getText().toString()); diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java index 4cf1f27795..b62331fec9 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java @@ -7,6 +7,8 @@ import android.util.AttributeSet; import android.widget.Button; import android.widget.EditText; + +import app.revanced.integrations.shared.Utils; import app.revanced.integrations.shared.settings.Setting; import app.revanced.integrations.shared.Logger; @@ -33,6 +35,8 @@ public ResettableEditTextPreference(Context context) { @Override protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { super.onPrepareDialogBuilder(builder); + Utils.setEditTextDialogTheme(builder); + Setting setting = Setting.getSettingFromPath(getKey()); if (setting != null) { builder.setNeutralButton(str("revanced_settings_reset"), null); diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java index 2de654a2ba..7b00931995 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java @@ -381,6 +381,8 @@ private void addGeneralCategory(final Context context, PreferenceScreen screen) importExport = new EditTextPreference(context) { protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + Utils.setEditTextDialogTheme(builder); + builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> { Utils.setClipboard(getEditText().getText().toString()); }); diff --git a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/objects/SegmentCategoryListPreference.java b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/objects/SegmentCategoryListPreference.java index 09235e8504..61c40c05dc 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/objects/SegmentCategoryListPreference.java +++ b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/objects/SegmentCategoryListPreference.java @@ -21,6 +21,7 @@ import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; +@SuppressWarnings("deprecation") public class SegmentCategoryListPreference extends ListPreference { private final SegmentCategory category; private EditText mEditText; @@ -45,6 +46,8 @@ public SegmentCategoryListPreference(Context context, SegmentCategory category) @Override protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { try { + Utils.setEditTextDialogTheme(builder); + Context context = builder.getContext(); TableLayout table = new TableLayout(context); table.setOrientation(LinearLayout.HORIZONTAL); From 294bfa758a1036c50932b3061d4fa60be62ae9da Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 28 Sep 2024 00:22:42 -0400 Subject: [PATCH 70/91] Unofficial support for 19.39 --- .../youtube/patches/VideoInformation.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java b/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java index 0d7a29aa66..9df12b57d4 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java @@ -18,7 +18,7 @@ public final class VideoInformation { public interface PlaybackController { // Methods are added to YT classes during patching. boolean seekTo(long videoTime); - boolean seekToRelative(long videoTimeOffset); + void seekToRelative(long videoTimeOffset); } private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f; @@ -229,21 +229,19 @@ public static boolean seekTo(final long seekTime) { /** * Seeks a relative amount. Should always be used over {@link #seekTo(long)} * when the desired seek time is an offset of the current time. - * - * @noinspection UnusedReturnValue */ - public static boolean seekToRelative(long seekTime) { + public static void seekToRelative(long seekTime) { Utils.verifyOnMainThread(); try { Logger.printDebug(() -> "Seeking relative to: " + seekTime); - // Try regular playback controller first, and it will not succeed if casting. + // 19.39+ does not have a boolean return type for relative seek. + // But can call both methods and it works correctly for both situations. PlaybackController controller = playerControllerRef.get(); if (controller == null) { Logger.printDebug(() -> "Cannot seek relative as player controller is null"); } else { - if (controller.seekToRelative(seekTime)) return true; - Logger.printDebug(() -> "seekToRelative did not succeeded. Trying MXD."); + controller.seekToRelative(seekTime); } // Adjust the fine adjustment function so it's at least 1 second before/after. @@ -258,13 +256,11 @@ public static boolean seekToRelative(long seekTime) { controller = mdxPlayerDirectorRef.get(); if (controller == null) { Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null"); - return false; + } else { + controller.seekToRelative(adjustedSeekTime); } - - return controller.seekToRelative(adjustedSeekTime); } catch (Exception ex) { Logger.printException(() -> "Failed to seek relative", ex); - return false; } } From 50dbccf14ecb86dec199272a040d49c8b8d582e8 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 6 Oct 2024 06:47:53 -0400 Subject: [PATCH 71/91] Comments --- app/src/main/java/app/revanced/integrations/shared/Utils.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/shared/Utils.java b/app/src/main/java/app/revanced/integrations/shared/Utils.java index 4485d9c2ca..c00886a317 100644 --- a/app/src/main/java/app/revanced/integrations/shared/Utils.java +++ b/app/src/main/java/app/revanced/integrations/shared/Utils.java @@ -730,9 +730,6 @@ public static void sortPreferenceGroups(@NonNull PreferenceGroup group) { * ReVanced/revanced-patches#3061 *

* To prevent these issues, apply the Dialog theme corresponding to [Android library]. - * - * @param builder Alertdialog builder to apply theme to. - * When used in a method containing an override, it must be called before 'super'. */ public static void setEditTextDialogTheme(AlertDialog.Builder builder) { final int editTextDialogStyle = getResourceIdentifier( From 55ec5d169648b7257658e5999e20c50f5bb9da50 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 15 Oct 2024 01:54:01 -0400 Subject: [PATCH 72/91] Miniplayer: Add rounded corners setting if patching 19.36+ --- .../youtube/patches/MiniplayerPatch.java | 13 ++++++++----- .../integrations/youtube/settings/Settings.java | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java index 938358f8d9..a446a18230 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java @@ -123,6 +123,9 @@ public boolean isModern() { private static final boolean HIDE_REWIND_FORWARD_ENABLED = CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get(); + private static final boolean MINIPLAYER_ROUNDED_CORNERS_ENABLED = + CURRENT_TYPE.isModern() && Settings.MINIPLAYER_ROUNDED_CORNERS.get(); + /** * Remove a broken and always present subtitle text that is only * present with {@link MiniplayerType#MODERN_2}. Bug was fixed in 19.21. @@ -247,8 +250,8 @@ public static int setMiniplayerDefaultSize(int original) { * Injection point. */ public static float setMovementBoundFactor(float original) { - // Not clear if this is useful to customize or not. - // So for now just log this and keep whatever is the original value. + // Not clear if customizing this is useful or not. + // So for now just log this and use the original value. if (original != 1.0) Logger.printDebug(() -> "setMovementBoundFactor original: " + original); return original; @@ -266,10 +269,10 @@ public static boolean setDropShadow(boolean original) { /** * Injection point. */ - public static boolean setUseBackgroundViewOutlineProvider(boolean original) { - if (original) Logger.printDebug(() -> "setUseBackgroundViewOutlineProvider original: " + true); + public static boolean setRoundedCorners(boolean original) { + if (original) Logger.printDebug(() -> "setRoundedCorners original: " + true); - return original; + return MINIPLAYER_ROUNDED_CORNERS_ENABLED; } /** diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 84761ca33a..c82acce6af 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -144,6 +144,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true, new MiniplayerHideExpandCloseAvailability()); public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3)); public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1)); + public static final BooleanSetting MINIPLAYER_ROUNDED_CORNERS = new BooleanSetting("revanced_miniplayer_rounded_corners", TRUE, true, MINIPLAYER_ANY_MODERN); public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, MINIPLAYER_ANY_MODERN); public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, MINIPLAYER_TYPE.availability(MODERN_1)); From 79b8f302ea8b3547a9ad304c1d1deeaa63ce2004 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 15 Oct 2024 02:09:42 -0400 Subject: [PATCH 73/91] refactor --- .../youtube/patches/MiniplayerPatch.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java index a446a18230..63c73036e9 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java @@ -124,7 +124,7 @@ public boolean isModern() { CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get(); private static final boolean MINIPLAYER_ROUNDED_CORNERS_ENABLED = - CURRENT_TYPE.isModern() && Settings.MINIPLAYER_ROUNDED_CORNERS.get(); + Settings.MINIPLAYER_ROUNDED_CORNERS.get(); /** * Remove a broken and always present subtitle text that is only @@ -235,11 +235,25 @@ public static boolean enableMiniplayerDragAndDrop(boolean original) { return DRAG_AND_DROP_ENABLED; } + + /** + * Injection point. + */ + public static boolean setRoundedCorners(boolean original) { + if (original) Logger.printDebug(() -> "setRoundedCorners original: " + true); + + if (CURRENT_TYPE.isModern()) { + return MINIPLAYER_ROUNDED_CORNERS_ENABLED; + } + + return original; + } + /** * Injection point. */ public static int setMiniplayerDefaultSize(int original) { - if (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_2 || CURRENT_TYPE == MODERN_3) { + if (CURRENT_TYPE.isModern()) { return MINIPLAYER_SIZE; } @@ -266,15 +280,6 @@ public static boolean setDropShadow(boolean original) { return original; } - /** - * Injection point. - */ - public static boolean setRoundedCorners(boolean original) { - if (original) Logger.printDebug(() -> "setRoundedCorners original: " + true); - - return MINIPLAYER_ROUNDED_CORNERS_ENABLED; - } - /** * Injection point. */ From 694aacf41888746734f7a478d934ece2302cad98 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 15 Oct 2024 19:53:09 -0400 Subject: [PATCH 74/91] fix(Hide player buttons patch): fix 19.34 --- .../patches/HidePlayerButtonsPatch.java | 38 +++++++++++++++++-- .../youtube/settings/Settings.java | 2 +- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java index aa501280b3..9931b94e9d 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java @@ -1,17 +1,47 @@ package app.revanced.integrations.youtube.patches; +import android.view.View; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") public final class HidePlayerButtonsPatch { + private static final boolean HIDE_PLAYER_BUTTONS_ENABLED = Settings.HIDE_PLAYER_BUTTONS.get(); + + private static final int PREVIOUS_BUTTON_RESOURCE_ID = + Utils.getResourceIdentifier("player_control_previous_button", "id"); + + private static final int NEXT_BUTTON_RESOURCE_ID = + Utils.getResourceIdentifier("player_control_next_button", "id"); + /** * Injection point. */ - public static boolean previousOrNextButtonIsVisible(boolean previousOrNextButtonVisible) { - if (Settings.HIDE_PLAYER_BUTTONS.get()) { - return false; + public static void hidePreviousNextButtons(View parentView) { + if (!HIDE_PLAYER_BUTTONS_ENABLED) { + return; + } + + hideButton(parentView, PREVIOUS_BUTTON_RESOURCE_ID); + hideButton(parentView, NEXT_BUTTON_RESOURCE_ID); + } + + private static void hideButton(View parentView, int resourceId) { + try { + View nextPreviousButton = parentView.findViewById(resourceId); + + if (nextPreviousButton == null) { + Logger.printDebug(() -> "Could not find previous/next button"); + } else { + Logger.printDebug(() -> "Hiding previous/next button"); + Utils.hideViewBy0dpUnderCondition(true, nextPreviousButton); + } + } catch (Exception ex) { + Logger.printException(() -> "hideButton failure", ex); } - return previousOrNextButtonVisible; } + } diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index c82acce6af..982fc1d390 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -131,7 +131,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting DISABLE_LIKE_SUBSCRIBE_GLOW = new BooleanSetting("revanced_disable_like_subscribe_glow", FALSE); public static final BooleanSetting HIDE_AUTOPLAY_BUTTON = new BooleanSetting("revanced_hide_autoplay_button", TRUE, true); public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_hide_cast_button", TRUE, true); - public static final BooleanSetting HIDE_PLAYER_BUTTONS = new BooleanSetting("revanced_hide_player_buttons", FALSE); + public static final BooleanSetting HIDE_PLAYER_BUTTONS = new BooleanSetting("revanced_hide_player_buttons", FALSE, true); public static final BooleanSetting COPY_VIDEO_URL = new BooleanSetting("revanced_copy_video_url", FALSE); public static final BooleanSetting COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_copy_video_url_timestamp", TRUE); public static final BooleanSetting PLAYBACK_SPEED_DIALOG_BUTTON = new BooleanSetting("revanced_playback_speed_dialog_button", FALSE); From f7b8249bbbcef08a27d490d48ee41264928919fa Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:45:11 -0400 Subject: [PATCH 75/91] fix(Hide player buttons patch): Override on click listener --- .../youtube/patches/HidePlayerButtonsPatch.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java index 9931b94e9d..31dc076693 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java @@ -38,6 +38,13 @@ private static void hideButton(View parentView, int resourceId) { } else { Logger.printDebug(() -> "Hiding previous/next button"); Utils.hideViewBy0dpUnderCondition(true, nextPreviousButton); + + // Button is no longer visible, but the click listener needs to be cleared otherwise + // the button can still be pressed even though it's 0dp. + // + // The listener is added after this hook in the same target method. + // To keep thing simple, the listener can be cleared on a deferred main thread call. + Utils.runOnMainThread(() -> nextPreviousButton.setOnClickListener(null)); } } catch (Exception ex) { Logger.printException(() -> "hideButton failure", ex); From ea11907afb4d7a78a1ebc141da6981dd135be656 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:53:15 -0400 Subject: [PATCH 76/91] refactor: simplify --- .../patches/HidePlayerButtonsPatch.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java index 31dc076693..3f38769836 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java @@ -34,21 +34,19 @@ private static void hideButton(View parentView, int resourceId) { View nextPreviousButton = parentView.findViewById(resourceId); if (nextPreviousButton == null) { - Logger.printDebug(() -> "Could not find previous/next button"); - } else { - Logger.printDebug(() -> "Hiding previous/next button"); - Utils.hideViewBy0dpUnderCondition(true, nextPreviousButton); - - // Button is no longer visible, but the click listener needs to be cleared otherwise - // the button can still be pressed even though it's 0dp. - // - // The listener is added after this hook in the same target method. - // To keep thing simple, the listener can be cleared on a deferred main thread call. - Utils.runOnMainThread(() -> nextPreviousButton.setOnClickListener(null)); + Logger.printException(() -> "Could not find player previous/next button"); + return; } + + // Must use a deferred call to main thread to hide the button. + // Otherwise if set to hidden here then the layout crashes, + // and if set to 0dp the button still functions even though it's not visible. + Utils.runOnMainThread(() -> { + Logger.printDebug(() -> "Hiding previous/next button"); + Utils.hideViewByRemovingFromParentUnderCondition(true, nextPreviousButton); + }); } catch (Exception ex) { Logger.printException(() -> "hideButton failure", ex); } } - } From 7fa079e4a74e74557dbcfabf340d3ae246672cf8 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:04:15 -0400 Subject: [PATCH 77/91] fix: Allow double tapping area where forward/back normally is. --- .../patches/HidePlayerButtonsPatch.java | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java index 3f38769836..bc876cc327 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java @@ -11,11 +11,11 @@ public final class HidePlayerButtonsPatch { private static final boolean HIDE_PLAYER_BUTTONS_ENABLED = Settings.HIDE_PLAYER_BUTTONS.get(); - private static final int PREVIOUS_BUTTON_RESOURCE_ID = - Utils.getResourceIdentifier("player_control_previous_button", "id"); + private static final int PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID = + Utils.getResourceIdentifier("player_control_previous_button_touch_area", "id"); - private static final int NEXT_BUTTON_RESOURCE_ID = - Utils.getResourceIdentifier("player_control_next_button", "id"); + private static final int PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID = + Utils.getResourceIdentifier("player_control_next_button_touch_area", "id"); /** * Injection point. @@ -25,28 +25,23 @@ public static void hidePreviousNextButtons(View parentView) { return; } - hideButton(parentView, PREVIOUS_BUTTON_RESOURCE_ID); - hideButton(parentView, NEXT_BUTTON_RESOURCE_ID); + // Must use a deferred call to main thread to hide the button. + // Otherwise the layout crashes if set to hidden now. + Utils.runOnMainThread(() -> { + hideView(parentView, PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID); + hideView(parentView, PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID); + }); } - private static void hideButton(View parentView, int resourceId) { - try { - View nextPreviousButton = parentView.findViewById(resourceId); - - if (nextPreviousButton == null) { - Logger.printException(() -> "Could not find player previous/next button"); - return; - } - - // Must use a deferred call to main thread to hide the button. - // Otherwise if set to hidden here then the layout crashes, - // and if set to 0dp the button still functions even though it's not visible. - Utils.runOnMainThread(() -> { - Logger.printDebug(() -> "Hiding previous/next button"); - Utils.hideViewByRemovingFromParentUnderCondition(true, nextPreviousButton); - }); - } catch (Exception ex) { - Logger.printException(() -> "hideButton failure", ex); + private static void hideView(View parentView, int resourceId) { + View nextPreviousButton = parentView.findViewById(resourceId); + + if (nextPreviousButton == null) { + Logger.printException(() -> "Could not find player previous/next button"); + return; } + + Logger.printDebug(() -> "Hiding previous/next button"); + Utils.hideViewByRemovingFromParentUnderCondition(true, nextPreviousButton); } } From bbd612690e0dd5720a0d5888617e395c9e749a0a Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 17 Oct 2024 23:10:04 -0400 Subject: [PATCH 78/91] feat(Theme): Use Cairo seekbar --- .../patches/theme/SeekbarColorPatch.java | 49 ++++++++++++++++--- .../youtube/settings/Settings.java | 33 +++++++------ 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java index 17caa5f6dc..ac59552a96 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java @@ -4,14 +4,34 @@ import android.graphics.Color; -import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.shared.settings.Setting; +import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") public final class SeekbarColorPatch { - private static final boolean USE_SEEKBAR_CUSTOM_COLOR = Settings.SEEKBAR_CUSTOM_COLOR.get(); + private static final boolean APP_SUPPORTS_CAIRO_SEEKBAR = + Utils.getAppVersionName().compareTo("19.23.00") >= 0; + + private static final boolean SEEKBAR_CAIRO_ENABLED = + APP_SUPPORTS_CAIRO_SEEKBAR && Settings.SEEKBAR_CAIRO.get(); + + private static final boolean SEEKBAR_CUSTOM_COLOR_ENABLED = + !SEEKBAR_CAIRO_ENABLED && Settings.SEEKBAR_CUSTOM_COLOR.get(); + + public static final class SeekbarCustomColorAvailability implements Setting.Availability { + public boolean isAvailable() { + return !APP_SUPPORTS_CAIRO_SEEKBAR || !Settings.SEEKBAR_CAIRO.get(); + } + } + + public static final class SeekbarCustomColorValueAvailability implements Setting.Availability { + public boolean isAvailable() { + return Settings.SEEKBAR_CUSTOM_COLOR.isAvailable() && Settings.SEEKBAR_CUSTOM_COLOR.get(); + } + } /** * Default color of the seekbar. @@ -40,7 +60,7 @@ public final class SeekbarColorPatch { Color.colorToHSV(ORIGINAL_SEEKBAR_COLOR, hsv); ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS = hsv[2]; - if (USE_SEEKBAR_CUSTOM_COLOR) { + if (!SEEKBAR_CAIRO_ENABLED && SEEKBAR_CUSTOM_COLOR_ENABLED) { loadCustomSeekbarColor(); } } @@ -60,6 +80,10 @@ public static int getSeekbarColor() { return seekbarColor; } + public static boolean cairoSeekbarEnabled(boolean original) { + if (original) Logger.printDebug(() -> "cairoSeekbarEnabled original: " + true); + return SEEKBAR_CAIRO_ENABLED; + } /** * Injection point. @@ -70,7 +94,7 @@ public static int getSeekbarColor() { * If {@link Settings#HIDE_SEEKBAR_THUMBNAIL} is enabled, this returns a fully transparent color. */ public static int getLithoColor(int colorValue) { - if (colorValue == ORIGINAL_SEEKBAR_COLOR) { + if (SEEKBAR_CUSTOM_COLOR_ENABLED && colorValue == ORIGINAL_SEEKBAR_COLOR) { if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) { return 0x00000000; } @@ -85,6 +109,10 @@ public static int getLithoColor(int colorValue) { * Overrides color when video player seekbar is clicked. */ public static int getVideoPlayerSeekbarClickedColor(int colorValue) { + if (!SEEKBAR_CUSTOM_COLOR_ENABLED) { + return colorValue; + } + return colorValue == ORIGINAL_SEEKBAR_COLOR ? getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR) : colorValue; @@ -96,6 +124,10 @@ public static int getVideoPlayerSeekbarClickedColor(int colorValue) { * Overrides color used for the video player seekbar. */ public static int getVideoPlayerSeekbarColor(int originalColor) { + if (!SEEKBAR_CUSTOM_COLOR_ENABLED) { + return originalColor; + } + return getSeekbarColorValue(originalColor); } @@ -105,9 +137,10 @@ public static int getVideoPlayerSeekbarColor(int originalColor) { */ private static int getSeekbarColorValue(int originalColor) { try { - if (!USE_SEEKBAR_CUSTOM_COLOR || originalColor == seekbarColor) { + if (!SEEKBAR_CUSTOM_COLOR_ENABLED || originalColor == seekbarColor) { return originalColor; // nothing to do } + final int alphaDifference = Color.alpha(originalColor) - Color.alpha(ORIGINAL_SEEKBAR_COLOR); // The seekbar uses the same color but different brightness for different situations. @@ -131,11 +164,13 @@ private static int getSeekbarColorValue(int originalColor) { } } - static int clamp(int value, int lower, int upper) { + /** @noinspection SameParameterValue */ + private static int clamp(int value, int lower, int upper) { return Math.max(lower, Math.min(value, upper)); } - static float clamp(float value, float lower, float upper) { + /** @noinspection SameParameterValue */ + private static float clamp(float value, float lower, float upper) { return Math.max(lower, Math.min(value, upper)); } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 9f37ba6122..507a6970c6 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -1,5 +1,18 @@ package app.revanced.integrations.youtube.settings; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static app.revanced.integrations.shared.settings.Setting.*; +import static app.revanced.integrations.youtube.patches.ChangeStartPagePatch.StartPage; +import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerHideExpandCloseAvailability; +import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType; +import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*; +import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.settings.*; import app.revanced.integrations.shared.settings.preference.SharedPrefCategory; @@ -10,21 +23,10 @@ import app.revanced.integrations.youtube.patches.spoof.ClientType; import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch; import app.revanced.integrations.youtube.patches.spoof.SpoofVideoStreamsPatch; +import app.revanced.integrations.youtube.patches.theme.SeekbarColorPatch.SeekbarCustomColorAvailability; +import app.revanced.integrations.youtube.patches.theme.SeekbarColorPatch.SeekbarCustomColorValueAvailability; import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import static app.revanced.integrations.shared.settings.Setting.*; -import static app.revanced.integrations.youtube.patches.ChangeStartPagePatch.StartPage; -import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerHideExpandCloseAvailability; -import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType; -import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*; -import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*; -import static java.lang.Boolean.FALSE; -import static java.lang.Boolean.TRUE; - @SuppressWarnings("deprecation") public class Settings extends BaseSettings { // Video @@ -254,8 +256,9 @@ public class Settings extends BaseSettings { public static final BooleanSetting RESTORE_OLD_SEEKBAR_THUMBNAILS = new BooleanSetting("revanced_restore_old_seekbar_thumbnails", TRUE); public static final BooleanSetting HIDE_SEEKBAR = new BooleanSetting("revanced_hide_seekbar", FALSE, true); public static final BooleanSetting HIDE_SEEKBAR_THUMBNAIL = new BooleanSetting("revanced_hide_seekbar_thumbnail", FALSE); - public static final BooleanSetting SEEKBAR_CUSTOM_COLOR = new BooleanSetting("revanced_seekbar_custom_color", FALSE, true); - public static final StringSetting SEEKBAR_CUSTOM_COLOR_VALUE = new StringSetting("revanced_seekbar_custom_color_value", "#FF0000", true, parent(SEEKBAR_CUSTOM_COLOR)); + public static final BooleanSetting SEEKBAR_CAIRO = new BooleanSetting("revanced_seekbar_cairo", FALSE, true); + public static final BooleanSetting SEEKBAR_CUSTOM_COLOR = new BooleanSetting("revanced_seekbar_custom_color", FALSE, true, new SeekbarCustomColorAvailability()); + public static final StringSetting SEEKBAR_CUSTOM_COLOR_VALUE = new StringSetting("revanced_seekbar_custom_color_value", "#FF0000", true, new SeekbarCustomColorValueAvailability()); // Misc public static final BooleanSetting AUTO_CAPTIONS = new BooleanSetting("revanced_auto_captions", FALSE); From 694edf65ca9fa20866139d712644bc9bb411f692 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 18 Oct 2024 05:40:46 -0400 Subject: [PATCH 79/91] refactor: Simplify seekbar cairo setting --- .../patches/theme/SeekbarColorPatch.java | 32 +++++-------------- .../youtube/settings/Settings.java | 7 ++-- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java index ac59552a96..bc2a720a14 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java @@ -6,32 +6,12 @@ import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; -import app.revanced.integrations.shared.settings.Setting; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") public final class SeekbarColorPatch { - private static final boolean APP_SUPPORTS_CAIRO_SEEKBAR = - Utils.getAppVersionName().compareTo("19.23.00") >= 0; - - private static final boolean SEEKBAR_CAIRO_ENABLED = - APP_SUPPORTS_CAIRO_SEEKBAR && Settings.SEEKBAR_CAIRO.get(); - - private static final boolean SEEKBAR_CUSTOM_COLOR_ENABLED = - !SEEKBAR_CAIRO_ENABLED && Settings.SEEKBAR_CUSTOM_COLOR.get(); - - public static final class SeekbarCustomColorAvailability implements Setting.Availability { - public boolean isAvailable() { - return !APP_SUPPORTS_CAIRO_SEEKBAR || !Settings.SEEKBAR_CAIRO.get(); - } - } - - public static final class SeekbarCustomColorValueAvailability implements Setting.Availability { - public boolean isAvailable() { - return Settings.SEEKBAR_CUSTOM_COLOR.isAvailable() && Settings.SEEKBAR_CUSTOM_COLOR.get(); - } - } + private static final boolean SEEKBAR_CUSTOM_COLOR_ENABLED = Settings.SEEKBAR_CUSTOM_COLOR.get(); /** * Default color of the seekbar. @@ -60,7 +40,7 @@ public boolean isAvailable() { Color.colorToHSV(ORIGINAL_SEEKBAR_COLOR, hsv); ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS = hsv[2]; - if (!SEEKBAR_CAIRO_ENABLED && SEEKBAR_CUSTOM_COLOR_ENABLED) { + if (SEEKBAR_CUSTOM_COLOR_ENABLED) { loadCustomSeekbarColor(); } } @@ -81,8 +61,12 @@ public static int getSeekbarColor() { } public static boolean cairoSeekbarEnabled(boolean original) { - if (original) Logger.printDebug(() -> "cairoSeekbarEnabled original: " + true); - return SEEKBAR_CAIRO_ENABLED; + if (original) { + Logger.printDebug(() -> "cairoSeekbarEnabled original: " + true); + if (SEEKBAR_CUSTOM_COLOR_ENABLED) return false; + } + + return original; } /** diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 507a6970c6..9e668e1da6 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -23,8 +23,6 @@ import app.revanced.integrations.youtube.patches.spoof.ClientType; import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch; import app.revanced.integrations.youtube.patches.spoof.SpoofVideoStreamsPatch; -import app.revanced.integrations.youtube.patches.theme.SeekbarColorPatch.SeekbarCustomColorAvailability; -import app.revanced.integrations.youtube.patches.theme.SeekbarColorPatch.SeekbarCustomColorValueAvailability; import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings; @SuppressWarnings("deprecation") @@ -256,9 +254,8 @@ public class Settings extends BaseSettings { public static final BooleanSetting RESTORE_OLD_SEEKBAR_THUMBNAILS = new BooleanSetting("revanced_restore_old_seekbar_thumbnails", TRUE); public static final BooleanSetting HIDE_SEEKBAR = new BooleanSetting("revanced_hide_seekbar", FALSE, true); public static final BooleanSetting HIDE_SEEKBAR_THUMBNAIL = new BooleanSetting("revanced_hide_seekbar_thumbnail", FALSE); - public static final BooleanSetting SEEKBAR_CAIRO = new BooleanSetting("revanced_seekbar_cairo", FALSE, true); - public static final BooleanSetting SEEKBAR_CUSTOM_COLOR = new BooleanSetting("revanced_seekbar_custom_color", FALSE, true, new SeekbarCustomColorAvailability()); - public static final StringSetting SEEKBAR_CUSTOM_COLOR_VALUE = new StringSetting("revanced_seekbar_custom_color_value", "#FF0000", true, new SeekbarCustomColorValueAvailability()); + public static final BooleanSetting SEEKBAR_CUSTOM_COLOR = new BooleanSetting("revanced_seekbar_custom_color", FALSE, true); + public static final StringSetting SEEKBAR_CUSTOM_COLOR_VALUE = new StringSetting("revanced_seekbar_custom_color_value", "#FF0000", true); // Misc public static final BooleanSetting AUTO_CAPTIONS = new BooleanSetting("revanced_auto_captions", FALSE); From 2b9150ede554ab10cb487e20791e7556ade26f45 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 18 Oct 2024 07:11:59 -0400 Subject: [PATCH 80/91] refactor: Use more descriptive fingerprint name --- .../integrations/youtube/patches/theme/SeekbarColorPatch.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java index bc2a720a14..0ec9fa8a93 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java @@ -60,9 +60,9 @@ public static int getSeekbarColor() { return seekbarColor; } - public static boolean cairoSeekbarEnabled(boolean original) { + public static boolean playerSeekbarGraidentEnabled(boolean original) { if (original) { - Logger.printDebug(() -> "cairoSeekbarEnabled original: " + true); + Logger.printDebug(() -> "playerSeekbarGraidentEnabled original: " + true); if (SEEKBAR_CUSTOM_COLOR_ENABLED) return false; } From 6b90fbd84d9cb7f3c4de48e4fa5edc6d150e947d Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 18 Oct 2024 22:39:25 -0400 Subject: [PATCH 81/91] fix typo --- .../integrations/youtube/patches/theme/SeekbarColorPatch.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java index 0ec9fa8a93..967ee97d3d 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java @@ -60,9 +60,9 @@ public static int getSeekbarColor() { return seekbarColor; } - public static boolean playerSeekbarGraidentEnabled(boolean original) { + public static boolean playerSeekbarGradientEnabled(boolean original) { if (original) { - Logger.printDebug(() -> "playerSeekbarGraidentEnabled original: " + true); + Logger.printDebug(() -> "playerSeekbarGradientEnabled original: " + true); if (SEEKBAR_CUSTOM_COLOR_ENABLED) return false; } From 934f860fbe37c2b139b815f1c79b6193fdba7b4c Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 18 Oct 2024 23:39:49 -0400 Subject: [PATCH 82/91] fix(Seekbar color): Support gradient seekbar --- .../patches/theme/SeekbarColorPatch.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java index 967ee97d3d..73150ef07e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java @@ -4,6 +4,8 @@ import android.graphics.Color; +import java.util.Arrays; + import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.settings.Settings; @@ -18,6 +20,16 @@ public final class SeekbarColorPatch { */ private static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0000; + /** + * Default colors of the gradient seekbar. + */ + private static final int[] ORIGINAL_SEEKBAR_GRADIENT_COLORS = { 0xFFFF0033, 0xFFFF2791 }; + + /** + * Default positions of the gradient seekbar. + */ + private static final float[] ORIGINAL_SEEKBAR_GRADIENT_POSITIONS = { 0.8f, 1.0f }; + /** * Default YouTube seekbar color brightness. */ @@ -87,6 +99,23 @@ public static int getLithoColor(int colorValue) { return colorValue; } + /** + * Injection point. + */ + public static void setLinearGradient(int[] colors, float[] positions) { + if (SEEKBAR_CUSTOM_COLOR_ENABLED) { + Logger.printDebug(() -> "colors: " + Arrays.toString(colors) + " positions: " + Arrays.toString(positions)); + + // All usage of linear gradients is hooked, so must identify the values used for the seekbar. + if (Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_COLORS, colors) + && Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_POSITIONS, positions)) { + Arrays.fill(colors, Settings.HIDE_SEEKBAR_THUMBNAIL.get() + ? 0x00000000 + : seekbarColor); + } + } + } + /** * Injection point. * From f6ff69db9895f0b9a0f3edd770e7dc2e68af5683 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 18 Oct 2024 23:58:01 -0400 Subject: [PATCH 83/91] refactor --- .../youtube/patches/theme/SeekbarColorPatch.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java index 73150ef07e..66de313ab0 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java @@ -104,15 +104,17 @@ public static int getLithoColor(int colorValue) { */ public static void setLinearGradient(int[] colors, float[] positions) { if (SEEKBAR_CUSTOM_COLOR_ENABLED) { - Logger.printDebug(() -> "colors: " + Arrays.toString(colors) + " positions: " + Arrays.toString(positions)); - - // All usage of linear gradients is hooked, so must identify the values used for the seekbar. + // Most litho usage of linear gradients is hooked here, + // so must only change if the values are those for the seekbar. if (Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_COLORS, colors) && Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_POSITIONS, positions)) { Arrays.fill(colors, Settings.HIDE_SEEKBAR_THUMBNAIL.get() ? 0x00000000 : seekbarColor); + return; } + + Logger.printDebug(() -> "colors: " + Arrays.toString(colors) + " positions: " + Arrays.toString(positions)); } } From 1c953eb5d1ac774ead7717490c2d79ad3af5fb85 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 19 Oct 2024 00:11:33 -0400 Subject: [PATCH 84/91] refactor: Move version checks to shared patch --- .../integrations/youtube/patches/MiniplayerPatch.java | 6 +----- .../integrations/youtube/shared/VersionCheck.java | 10 ++++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/app/revanced/integrations/youtube/shared/VersionCheck.java diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java index 63c73036e9..af9194934d 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java @@ -2,6 +2,7 @@ import static app.revanced.integrations.shared.StringRef.str; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*; +import static app.revanced.integrations.youtube.shared.VersionCheck.*; import android.util.DisplayMetrics; import android.view.View; @@ -87,11 +88,6 @@ public boolean isModern() { MINIPLAYER_SIZE = dipWidth; } - private static final boolean IS_19_20_OR_GREATER = Utils.getAppVersionName().compareTo("19.20.00") >= 0; - private static final boolean IS_19_21_OR_GREATER = Utils.getAppVersionName().compareTo("19.21.00") >= 0; - private static final boolean IS_19_26_OR_GREATER = Utils.getAppVersionName().compareTo("19.26.00") >= 0; - private static final boolean IS_19_29_OR_GREATER = Utils.getAppVersionName().compareTo("19.29.00") >= 0; - /** * Modern subtitle overlay for {@link MiniplayerType#MODERN_2}. * Resource is not present in older targets, and this field will be zero. diff --git a/app/src/main/java/app/revanced/integrations/youtube/shared/VersionCheck.java b/app/src/main/java/app/revanced/integrations/youtube/shared/VersionCheck.java new file mode 100644 index 0000000000..364d00c0c9 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/shared/VersionCheck.java @@ -0,0 +1,10 @@ +package app.revanced.integrations.youtube.shared; + +import app.revanced.integrations.shared.Utils; + +public class VersionCheck { + public static final boolean IS_19_20_OR_GREATER = Utils.getAppVersionName().compareTo("19.20.00") >= 0; + public static final boolean IS_19_21_OR_GREATER = Utils.getAppVersionName().compareTo("19.21.00") >= 0; + public static final boolean IS_19_26_OR_GREATER = Utils.getAppVersionName().compareTo("19.26.00") >= 0; + public static final boolean IS_19_29_OR_GREATER = Utils.getAppVersionName().compareTo("19.29.00") >= 0; +} From 0e689739a8c30900bce2cb1be33f83a572f4b739 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 19 Oct 2024 00:18:45 -0400 Subject: [PATCH 85/91] logging --- .../integrations/youtube/patches/theme/SeekbarColorPatch.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java index 66de313ab0..43f80378cc 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java @@ -114,7 +114,8 @@ public static void setLinearGradient(int[] colors, float[] positions) { return; } - Logger.printDebug(() -> "colors: " + Arrays.toString(colors) + " positions: " + Arrays.toString(positions)); + Logger.printDebug(() -> "Ignoring gradient colors: " + Arrays.toString(colors) + + " positions: " + Arrays.toString(positions)); } } From 021ef7963d3fdec2d15fc9e8b3f8aaa315d9a1c5 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 19 Oct 2024 00:26:43 -0400 Subject: [PATCH 86/91] refactor: rename --- .../revanced/integrations/youtube/patches/MiniplayerPatch.java | 2 +- .../shared/{VersionCheck.java => VersionCheckPatch.java} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/src/main/java/app/revanced/integrations/youtube/shared/{VersionCheck.java => VersionCheckPatch.java} (94%) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java index af9194934d..6428e29a82 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java @@ -2,7 +2,7 @@ import static app.revanced.integrations.shared.StringRef.str; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*; -import static app.revanced.integrations.youtube.shared.VersionCheck.*; +import static app.revanced.integrations.youtube.shared.VersionCheckPatch.*; import android.util.DisplayMetrics; import android.view.View; diff --git a/app/src/main/java/app/revanced/integrations/youtube/shared/VersionCheck.java b/app/src/main/java/app/revanced/integrations/youtube/shared/VersionCheckPatch.java similarity index 94% rename from app/src/main/java/app/revanced/integrations/youtube/shared/VersionCheck.java rename to app/src/main/java/app/revanced/integrations/youtube/shared/VersionCheckPatch.java index 364d00c0c9..19e281678b 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/shared/VersionCheck.java +++ b/app/src/main/java/app/revanced/integrations/youtube/shared/VersionCheckPatch.java @@ -2,7 +2,7 @@ import app.revanced.integrations.shared.Utils; -public class VersionCheck { +public class VersionCheckPatch { public static final boolean IS_19_20_OR_GREATER = Utils.getAppVersionName().compareTo("19.20.00") >= 0; public static final boolean IS_19_21_OR_GREATER = Utils.getAppVersionName().compareTo("19.21.00") >= 0; public static final boolean IS_19_26_OR_GREATER = Utils.getAppVersionName().compareTo("19.26.00") >= 0; From ec4cdc5fc75f990c3bd687c165b6df13150a3b60 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 19 Oct 2024 00:33:42 -0400 Subject: [PATCH 87/91] fix seekbar not hiding if custom seekbar color is off --- .../youtube/patches/theme/SeekbarColorPatch.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java index 43f80378cc..001ee7e402 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java @@ -90,11 +90,14 @@ public static boolean playerSeekbarGradientEnabled(boolean original) { * If {@link Settings#HIDE_SEEKBAR_THUMBNAIL} is enabled, this returns a fully transparent color. */ public static int getLithoColor(int colorValue) { - if (SEEKBAR_CUSTOM_COLOR_ENABLED && colorValue == ORIGINAL_SEEKBAR_COLOR) { + if (colorValue == ORIGINAL_SEEKBAR_COLOR) { if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) { return 0x00000000; } - return getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR); + + if (SEEKBAR_CUSTOM_COLOR_ENABLED) { + return getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR); + } } return colorValue; } From 611f7c4e2120ab99a047cada004a7a996085ab63 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 19 Oct 2024 00:34:42 -0400 Subject: [PATCH 88/91] fix: simplify --- .../integrations/youtube/patches/theme/SeekbarColorPatch.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java index 001ee7e402..2e97a27d86 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java @@ -95,9 +95,7 @@ public static int getLithoColor(int colorValue) { return 0x00000000; } - if (SEEKBAR_CUSTOM_COLOR_ENABLED) { - return getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR); - } + return getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR); } return colorValue; } From f2770d70a35fd1e60ac81ec3e45a83d444f30846 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 19 Oct 2024 00:37:26 -0400 Subject: [PATCH 89/91] fix typo --- .../app/revanced/integrations/youtube/settings/Settings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 9e668e1da6..85af3e46f9 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -255,7 +255,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_SEEKBAR = new BooleanSetting("revanced_hide_seekbar", FALSE, true); public static final BooleanSetting HIDE_SEEKBAR_THUMBNAIL = new BooleanSetting("revanced_hide_seekbar_thumbnail", FALSE); public static final BooleanSetting SEEKBAR_CUSTOM_COLOR = new BooleanSetting("revanced_seekbar_custom_color", FALSE, true); - public static final StringSetting SEEKBAR_CUSTOM_COLOR_VALUE = new StringSetting("revanced_seekbar_custom_color_value", "#FF0000", true); + public static final StringSetting SEEKBAR_CUSTOM_COLOR_VALUE = new StringSetting("revanced_seekbar_custom_color_value", "#FF0000", true, parent(SEEKBAR_CUSTOM_COLOR)); // Misc public static final BooleanSetting AUTO_CAPTIONS = new BooleanSetting("revanced_auto_captions", FALSE); From 97d9eac7c4d9ef7ed8654be941561c53f303eeb2 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 19 Oct 2024 03:34:23 -0400 Subject: [PATCH 90/91] logging --- .../youtube/patches/ReturnYouTubeDislikePatch.java | 3 +++ .../youtube/returnyoutubedislike/ReturnYouTubeDislike.java | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java index ec52ef076f..3a47ef52cc 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java @@ -699,10 +699,12 @@ public static void sendVote(int vote) { if (!Settings.RYD_ENABLED.get()) { return; } + final boolean isNoneHiddenOrMinimized = PlayerType.getCurrent().isNoneHiddenOrMinimized(); if (isNoneHiddenOrMinimized && !Settings.RYD_SHORTS.get()) { return; } + ReturnYouTubeDislike videoData = currentVideoData; if (videoData == null) { Logger.printDebug(() -> "Cannot send vote, as current video data is null"); @@ -723,6 +725,7 @@ public static void sendVote(int vote) { return; } } + Logger.printException(() -> "Unknown vote type: " + vote); } catch (Exception ex) { Logger.printException(() -> "sendVote failure", ex); diff --git a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java index b63d0484e0..8b97077cbe 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java @@ -585,8 +585,13 @@ && spansHaveEqualTextAndColor(original, originalDislikeSpan)) { public void sendVote(@NonNull Vote vote) { Utils.verifyOnMainThread(); Objects.requireNonNull(vote); + try { - if (isShort != PlayerType.getCurrent().isNoneOrHidden()) { + PlayerType currentType = PlayerType.getCurrent(); + if (isShort != currentType.isNoneHiddenOrMinimized()) { + Logger.printDebug(() -> "Cannot vote for video: " + videoId + + " as current player type does not match: " + currentType); + // Shorts was loaded with regular video present, then Shorts was closed. // and then user voted on the now visible original video. // Cannot send a vote, because this instance is for the wrong video. From de105bc78e9a4fe62da8dc91aea6de13f5f42fc9 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 19 Oct 2024 04:10:41 -0400 Subject: [PATCH 91/91] refactor: Move patch to patch package --- .../revanced/integrations/youtube/patches/MiniplayerPatch.java | 2 +- .../youtube/{shared => patches}/VersionCheckPatch.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/src/main/java/app/revanced/integrations/youtube/{shared => patches}/VersionCheckPatch.java (91%) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java index 6428e29a82..9af9fd5e8d 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java @@ -2,7 +2,7 @@ import static app.revanced.integrations.shared.StringRef.str; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*; -import static app.revanced.integrations.youtube.shared.VersionCheckPatch.*; +import static app.revanced.integrations.youtube.patches.VersionCheckPatch.*; import android.util.DisplayMetrics; import android.view.View; diff --git a/app/src/main/java/app/revanced/integrations/youtube/shared/VersionCheckPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/VersionCheckPatch.java similarity index 91% rename from app/src/main/java/app/revanced/integrations/youtube/shared/VersionCheckPatch.java rename to app/src/main/java/app/revanced/integrations/youtube/patches/VersionCheckPatch.java index 19e281678b..4ede010158 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/shared/VersionCheckPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/VersionCheckPatch.java @@ -1,4 +1,4 @@ -package app.revanced.integrations.youtube.shared; +package app.revanced.integrations.youtube.patches; import app.revanced.integrations.shared.Utils;