chore: Merge branch dev to main (#521)

This commit is contained in:
oSumAtrIX 2023-11-20 18:56:05 +01:00 committed by GitHub
commit c8510dbb4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 165 additions and 108 deletions

View File

@ -1,3 +1,17 @@
## [0.122.2-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v0.122.2-dev.1...v0.122.2-dev.2) (2023-11-20)
### Bug Fixes
* **YouTube - Spoof client:** Fix frozen video on playback start ([#520](https://github.com/ReVanced/revanced-integrations/issues/520)) ([9139954](https://github.com/ReVanced/revanced-integrations/commit/91399540ba55c80c437652bf919d5af7b080bdfb))
## [0.122.2-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.122.1...v0.122.2-dev.1) (2023-11-19)
### Bug Fixes
* **YouTube - Hide layout components:** Check if `bytes` parameter of `LayoutComponentsFilter#filterMixPlaylists` for null ([3eb07c1](https://github.com/ReVanced/revanced-integrations/commit/3eb07c12dda889f6ed50fb3b6900b2d789bb0c10))
## [0.122.1](https://github.com/ReVanced/revanced-integrations/compare/v0.122.0...v0.122.1) (2023-11-19)

View File

@ -0,0 +1,12 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public class DisableRollingNumberAnimationsPatch {
/**
* Injection point.
*/
public static boolean disableRollingNumberAnimations() {
return SettingsEnum.DISABLE_ROLLING_NUMBER_ANIMATIONS.getBoolean();
}
}

View File

@ -247,7 +247,12 @@ public final class LayoutComponentsFilter extends Filter {
* Injection point.
* Called from a different place then the other filters.
*/
public static boolean filterMixPlaylists(final Object conversionContext, final byte[] bytes) {
public static boolean filterMixPlaylists(final Object conversionContext, @Nullable final byte[] bytes) {
if (bytes == null) {
LogHelper.printDebug(() -> "bytes is null");
return false;
}
// Prevent playlist items being hidden, if a mix playlist is present in it.
if (mixPlaylistsExceptions.matches(conversionContext.toString()))
return false;

View File

@ -1,6 +1,5 @@
package app.revanced.integrations.patches.spoof;
import static app.revanced.integrations.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer;
import static app.revanced.integrations.utils.ReVancedUtils.containsAny;
import android.view.View;
@ -9,16 +8,11 @@ import android.widget.ImageView;
import androidx.annotation.Nullable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.patches.spoof.requests.StoryboardRendererRequester;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.shared.PlayerType;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
/** @noinspection unused*/
public class SpoofSignaturePatch {
@ -51,29 +45,16 @@ public class SpoofSignaturePatch {
/**
* Last video id loaded. Used to prevent reloading the same spec multiple times.
*/
@Nullable
private static volatile String lastPlayerResponseVideoId;
private static volatile Future<StoryboardRenderer> rendererFuture;
@Nullable
private static volatile StoryboardRenderer videoRenderer;
private static volatile boolean useOriginalStoryboardRenderer;
private static volatile boolean isPlayingShorts;
@Nullable
private static StoryboardRenderer getRenderer() {
if (rendererFuture != null) {
try {
return rendererFuture.get(2000, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) {
LogHelper.printDebug(() -> "Could not get renderer (get timed out)");
} catch (ExecutionException | InterruptedException ex) {
// Should never happen.
LogHelper.printException(() -> "Could not get renderer", ex);
}
}
return null;
}
/**
* Injection point.
*
@ -82,62 +63,64 @@ public class SpoofSignaturePatch {
* @param parameters Original protobuf parameter value.
*/
public static String spoofParameter(String parameters) {
LogHelper.printDebug(() -> "Original protobuf parameter value: " + parameters);
try {
LogHelper.printDebug(() -> "Original protobuf parameter value: " + parameters);
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) return parameters;
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) 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.
if (useOriginalStoryboardRenderer = parameters.length() > 150) 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.
if (useOriginalStoryboardRenderer = parameters.length() > 150) return parameters;
// Shorts do not need to be spoofed.
if (useOriginalStoryboardRenderer = parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) {
isPlayingShorts = true;
return parameters;
}
isPlayingShorts = false;
boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL
&& containsAny(parameters, AUTOPLAY_PARAMETERS);
if (isPlayingFeed) {
if (useOriginalStoryboardRenderer = !SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean()) {
// Don't spoof the feed video playback. This will cause video playback issues,
// but only if user continues watching for more than 1 minute.
// Shorts do not need to be spoofed.
if (useOriginalStoryboardRenderer = parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) {
isPlayingShorts = true;
return parameters;
}
// Spoof the feed video. Video will show up in watch history and video subtitles are missing.
fetchStoryboardRenderer();
return SCRIM_PARAMETER + INCOGNITO_PARAMETERS;
}
isPlayingShorts = false;
fetchStoryboardRenderer();
boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL
&& containsAny(parameters, AUTOPLAY_PARAMETERS);
if (isPlayingFeed) {
if (useOriginalStoryboardRenderer = !SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean()) {
// 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) {
LogHelper.printException(() -> "spoofParameter failure", ex);
}
return INCOGNITO_PARAMETERS;
}
private static void fetchStoryboardRenderer() {
if (!SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) {
lastPlayerResponseVideoId = null;
rendererFuture = null;
videoRenderer = null;
return;
}
String videoId = VideoInformation.getPlayerResponseVideoId();
if (!videoId.equals(lastPlayerResponseVideoId)) {
rendererFuture = ReVancedUtils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId));
lastPlayerResponseVideoId = videoId;
// This will block starting video playback until the fetch completes.
// This is desired because if this returns without finishing the fetch,
// then video will start playback but the image will be frozen
// while the main thread call for the renderer waits for the fetch to complete.
videoRenderer = StoryboardRendererRequester.getStoryboardRenderer(videoId);
}
// Block until the fetch is completed. Without this, occasionally when a new video is opened
// the video will be frozen a few seconds while the audio plays.
// This is because the main thread is calling to get the storyboard but the fetch is not completed.
// To prevent this, call get() here and block until the fetch is completed.
// So later when the main thread calls to get the renderer it will never block as the future is done.
getRenderer();
}
private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec,
boolean returnNullIfLiveStream) {
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
StoryboardRenderer renderer = getRenderer();
StoryboardRenderer renderer = videoRenderer;
if (renderer != null) {
if (returnNullIfLiveStream && renderer.isLiveStream()) return null;
return renderer.getSpec();
@ -171,7 +154,7 @@ public class SpoofSignaturePatch {
*/
public static int getRecommendedLevel(int originalLevel) {
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
StoryboardRenderer renderer = getRenderer();
StoryboardRenderer renderer = videoRenderer;
if (renderer != null) {
Integer recommendedLevel = renderer.getRecommendedLevel();
if (recommendedLevel != null) return recommendedLevel;
@ -195,15 +178,19 @@ public class SpoofSignaturePatch {
* @param view seekbar thumbnail view. Includes both shorts and regular videos.
*/
public static void seekbarImageViewCreated(ImageView view) {
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()
|| SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) {
return;
}
if (isPlayingShorts) return;
try {
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()
|| SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) {
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);
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) {
LogHelper.printException(() -> "seekbarImageViewCreated failure", ex);
}
}
}

View File

@ -23,6 +23,11 @@ final class PlayerRoutes {
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 = 4 * 1000; // 4 Seconds.
static {
JSONObject innerTubeBody = new JSONObject();
@ -88,8 +93,8 @@ final class PlayerRoutes {
connection.setUseCaches(false);
connection.setDoOutput(true);
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
return connection;
}
}

View File

@ -1,27 +1,48 @@
package app.revanced.integrations.patches.spoof.requests;
import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.ANDROID_INNER_TUBE_BODY;
import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.GET_STORYBOARD_SPEC_RENDERER;
import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.TV_EMBED_INNER_TUBE_BODY;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.patches.spoof.StoryboardRenderer;
import app.revanced.integrations.requests.Requester;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
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.patches.spoof.requests.PlayerRoutes.*;
import app.revanced.integrations.patches.spoof.StoryboardRenderer;
import app.revanced.integrations.requests.Requester;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
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;
ReVancedUtils.doNothingForDuration(maximumTimeToRandomlyWait);
}
}
private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex,
boolean showToastOnIOException) {
if (showToastOnIOException) ReVancedUtils.showToastShort(toastMessage);
LogHelper.printInfo(() -> toastMessage, ex);
}
@Nullable
private static JSONObject fetchPlayerResponse(@NonNull String requestBody) {
private static JSONObject fetchPlayerResponse(@NonNull String requestBody, boolean showToastOnIOException) {
final long startTime = System.currentTimeMillis();
try {
ReVancedUtils.verifyOffMainThread();
@ -33,14 +54,21 @@ public class StoryboardRendererRequester {
connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length);
final int responseCode = connection.getResponseCode();
randomlyWaitIfLocallyDebugging();
if (responseCode == 200) return Requester.parseJSONObject(connection);
LogHelper.printException(() -> "API not available: " + responseCode);
// Always show a toast for this, as a non 200 response means something is broken.
handleConnectionError("Spoof storyboard not available: " + responseCode,
null, showToastOnIOException || SettingsEnum.DEBUG_TOAST_ON_ERROR.getBoolean());
connection.disconnect();
} catch (SocketTimeoutException ex) {
LogHelper.printException(() -> "API timed out", ex);
handleConnectionError("Spoof storyboard temporarily not available (API timed out)",
ex, showToastOnIOException);
} catch (IOException ex) {
handleConnectionError("Spoof storyboard temporarily not available: " + ex.getMessage(),
ex, showToastOnIOException);
} catch (Exception ex) {
LogHelper.printException(() -> "Failed to fetch storyboard URL", ex);
LogHelper.printException(() -> "Spoof storyboard fetch failed", ex); // Should never happen.
} finally {
LogHelper.printDebug(() -> "Request took: " + (System.currentTimeMillis() - startTime) + "ms");
}
@ -64,8 +92,9 @@ public class StoryboardRendererRequester {
* @return StoryboardRenderer or null if playabilityStatus is not OK.
*/
@Nullable
private static StoryboardRenderer getStoryboardRendererUsingBody(@NonNull String innerTubeBody) {
final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody);
private static StoryboardRenderer getStoryboardRendererUsingBody(@NonNull String innerTubeBody,
boolean showToastOnIOException) {
final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody, showToastOnIOException);
if (playerResponse != null && isPlayabilityStatusOk(playerResponse))
return getStoryboardRendererUsingResponse(playerResponse);
@ -103,23 +132,19 @@ public class StoryboardRendererRequester {
@Nullable
public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) {
try {
Objects.requireNonNull(videoId);
Objects.requireNonNull(videoId);
var renderer = getStoryboardRendererUsingBody(String.format(ANDROID_INNER_TUBE_BODY, videoId));
var renderer = getStoryboardRendererUsingBody(
String.format(ANDROID_INNER_TUBE_BODY, videoId), false);
if (renderer == null) {
LogHelper.printDebug(() -> videoId + " not available using Android client");
renderer = getStoryboardRendererUsingBody(
String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId), true);
if (renderer == null) {
LogHelper.printDebug(() -> videoId + " not available using Android client");
renderer = getStoryboardRendererUsingBody(String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId));
if (renderer == null) {
LogHelper.printDebug(() -> videoId + " not available using TV embedded client");
}
LogHelper.printDebug(() -> videoId + " not available using TV embedded client");
}
return renderer;
} catch (Exception ex) {
LogHelper.printException(() -> "Failed to fetch storyboard URL", ex);
}
return null;
return renderer;
}
}

View File

@ -139,25 +139,13 @@ public class ReturnYouTubeDislikeApi {
* Simulates a slow response by doing meaningless calculations.
* Used to debug the app UI and verify UI timeout logic works
*/
@SuppressWarnings("UnusedReturnValue")
private static long randomlyWaitIfLocallyDebugging() {
private static void randomlyWaitIfLocallyDebugging() {
final boolean DEBUG_RANDOMLY_DELAY_NETWORK_CALLS = false; // set true to debug UI
if (DEBUG_RANDOMLY_DELAY_NETWORK_CALLS) {
final long amountOfTimeToWaste = (long) (Math.random()
* (API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS + API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS));
final long timeCalculationStarted = System.currentTimeMillis();
LogHelper.printDebug(() -> "Artificially creating network delay of: " + amountOfTimeToWaste + "ms");
long meaninglessValue = 0;
while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) {
// could do a thread sleep, but that will trigger an exception if the thread is interrupted
meaninglessValue += Long.numberOfLeadingZeros((long)Math.exp(Math.random()));
}
// return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work,
// leaving an empty loop that hammers on the System.currentTimeMillis native call
return meaninglessValue;
ReVancedUtils.doNothingForDuration(amountOfTimeToWaste);
}
return 0;
}
/**

View File

@ -62,6 +62,7 @@ public enum SettingsEnum {
CUSTOM_FILTER_STRINGS("revanced_custom_filter_strings", STRING, "", true, parents(CUSTOM_FILTER)),
DISABLE_FULLSCREEN_AMBIENT_MODE("revanced_disable_fullscreen_ambient_mode", BOOLEAN, TRUE, true),
DISABLE_RESUMING_SHORTS_PLAYER("revanced_disable_resuming_shorts_player", BOOLEAN, FALSE),
DISABLE_ROLLING_NUMBER_ANIMATIONS("revanced_disable_rolling_number_animations", BOOLEAN, FALSE),
DISABLE_SUGGESTED_VIDEO_END_SCREEN("revanced_disable_suggested_video_end_screen", BOOLEAN, TRUE),
GRADIENT_LOADING_SCREEN("revanced_gradient_loading_screen", BOOLEAN, FALSE),
HIDE_ALBUM_CARDS("revanced_hide_album_cards", BOOLEAN, FALSE, true),

View File

@ -112,6 +112,26 @@ public class ReVancedUtils {
return backgroundThreadPool.submit(call);
}
/**
* Simulates a delay by doing meaningless calculations.
* Used for debugging to verify UI timeout logic.
*/
@SuppressWarnings("UnusedReturnValue")
public static long doNothingForDuration(long amountOfTimeToWaste) {
final long timeCalculationStarted = System.currentTimeMillis();
LogHelper.printDebug(() -> "Artificially creating delay of: " + amountOfTimeToWaste + "ms");
long meaninglessValue = 0;
while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) {
// could do a thread sleep, but that will trigger an exception if the thread is interrupted
meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random()));
}
// return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work,
// leaving an empty loop that hammers on the System.currentTimeMillis native call
return meaninglessValue;
}
public static boolean containsAny(@NonNull String value, @NonNull String... targets) {
return indexOfFirstFound(value, targets) >= 0;
}

View File

@ -1,4 +1,4 @@
org.gradle.parallel = true
org.gradle.caching = true
android.useAndroidX = true
version = 0.122.1
version = 0.122.2-dev.2