feat(YouTube - Remember video quality): Add separate Shorts default quality settings (#4543)

This commit is contained in:
alieRN
2025-03-06 09:46:33 +03:00
committed by GitHub
parent 74ec83f964
commit 88142ab464
20 changed files with 312 additions and 87 deletions

View File

@ -158,16 +158,16 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
/**
* Syncs all UI Preferences to any {@link Setting} they represent.
*/
private void updatePreferenceScreen(@NonNull PreferenceScreen screen,
private void updatePreferenceScreen(@NonNull PreferenceGroup group,
boolean syncSettingValue,
boolean applySettingToPreference) {
// Alternatively this could iterate thru all Settings and check for any matching Preferences,
// but there are many more Settings than UI preferences so it's more efficient to only check
// the Preferences.
for (int i = 0, prefCount = screen.getPreferenceCount(); i < prefCount; i++) {
Preference pref = screen.getPreference(i);
if (pref instanceof PreferenceScreen) {
updatePreferenceScreen((PreferenceScreen) pref, syncSettingValue, applySettingToPreference);
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
Preference pref = group.getPreference(i);
if (pref instanceof PreferenceGroup subGroup) {
updatePreferenceScreen(subGroup, syncSettingValue, applySettingToPreference);
} else if (pref.hasKey()) {
String key = pref.getKey();
Setting<?> setting = Setting.getSettingFromPath(key);

View File

@ -0,0 +1,34 @@
package app.revanced.extension.shared.settings.preference;
import android.annotation.SuppressLint;
import android.content.Context;
import android.preference.PreferenceCategory;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/**
* Empty preference category with no title, used to organize and group related preferences together.
*/
@SuppressWarnings({"unused", "deprecation"})
public class NoTitlePreferenceCategory extends PreferenceCategory {
public NoTitlePreferenceCategory(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NoTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public NoTitlePreferenceCategory(Context context) {
super(context);
}
@Override
@SuppressLint("MissingSuperCall")
protected View onCreateView(ViewGroup parent) {
// Return an empty, zero-height view to eliminate spacing
return new View(getContext());
}
}

View File

@ -1,7 +1,7 @@
package app.revanced.extension.youtube.patches;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
@SuppressWarnings("unused")
public class BackgroundPlaybackPatch {
@ -23,16 +23,7 @@ public class BackgroundPlaybackPatch {
// 7. Close the Short
// 8. Resume playing the regular video
// 9. Minimize the app (PIP should appear)
if (!VideoInformation.lastVideoIdIsShort()) {
return true; // Definitely is not a Short.
}
// TODO: Add better hook.
// Might be a Shorts, or might be a prior regular video on screen again after a Shorts was closed.
// This incorrectly prevents PIP if player is in WATCH_WHILE_MINIMIZED after closing a Shorts,
// But there's no way around this unless an additional hook is added to definitively detect
// the Shorts player is on screen. This use case is unusual anyways so it's not a huge concern.
return !PlayerType.getCurrent().isNoneHiddenOrMinimized();
return !ShortsPlayerState.isOpen();
}
/**

View File

@ -1,7 +1,7 @@
package app.revanced.extension.youtube.patches;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
@SuppressWarnings("unused")
public class DisableAutoCaptionsPatch {
@ -14,7 +14,7 @@ public class DisableAutoCaptionsPatch {
public static boolean autoCaptionsEnabled() {
return Settings.AUTO_CAPTIONS.get()
// Do not use auto captions for Shorts.
&& !PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized();
&& ShortsPlayerState.isOpen();
}
}

View File

@ -1,8 +1,11 @@
package app.revanced.extension.youtube.patches;
import android.view.View;
import androidx.annotation.Nullable;
import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
import app.revanced.extension.youtube.shared.VideoState;
@SuppressWarnings("unused")
@ -24,4 +27,26 @@ public class PlayerTypeHookPatch {
VideoState.setFromString(youTubeVideoState.name());
}
/**
* Injection point.
*
* Add a listener to the shorts player overlay View.
* Triggered when a shorts player is attached or detached to Windows.
*
* @param view shorts player overlay (R.id.reel_watch_player).
*/
public static void onShortsCreate(View view) {
view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(@Nullable View v) {
ShortsPlayerState.setOpen(true);
}
@Override
public void onViewDetachedFromWindow(@Nullable View v) {
ShortsPlayerState.setOpen(false);
}
});
}
}

View File

@ -12,15 +12,19 @@ import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.IntegerSetting;
import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
@SuppressWarnings("unused")
public class RememberVideoQualityPatch {
private static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;
private static final IntegerSetting wifiQualitySetting = Settings.VIDEO_QUALITY_DEFAULT_WIFI;
private static final IntegerSetting mobileQualitySetting = Settings.VIDEO_QUALITY_DEFAULT_MOBILE;
private static final IntegerSetting videoQualityWifi = Settings.VIDEO_QUALITY_DEFAULT_WIFI;
private static final IntegerSetting videoQualityMobile = Settings.VIDEO_QUALITY_DEFAULT_MOBILE;
private static final IntegerSetting shortsQualityWifi = Settings.SHORTS_QUALITY_DEFAULT_WIFI;
private static final IntegerSetting shortsQualityMobile = Settings.SHORTS_QUALITY_DEFAULT_MOBILE;
private static boolean qualityNeedsUpdating;
@ -41,17 +45,29 @@ public class RememberVideoQualityPatch {
@Nullable
private static List<Integer> videoQualities;
private static boolean shouldRememberVideoQuality() {
BooleanSetting preference = ShortsPlayerState.isOpen() ?
Settings.REMEMBER_SHORTS_QUALITY_LAST_SELECTED
: Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED;
return preference.get();
}
private static void changeDefaultQuality(int defaultQuality) {
String networkTypeMessage;
boolean useShortsPreference = ShortsPlayerState.isOpen();
if (Utils.getNetworkType() == NetworkType.MOBILE) {
mobileQualitySetting.save(defaultQuality);
if (useShortsPreference) shortsQualityMobile.save(defaultQuality);
else videoQualityMobile.save(defaultQuality);
networkTypeMessage = str("revanced_remember_video_quality_mobile");
} else {
wifiQualitySetting.save(defaultQuality);
if (useShortsPreference) shortsQualityWifi.save(defaultQuality);
else videoQualityWifi.save(defaultQuality);
networkTypeMessage = str("revanced_remember_video_quality_wifi");
}
Utils.showToastShort(
str("revanced_remember_video_quality_toast", networkTypeMessage, (defaultQuality + "p")));
Utils.showToastShort(str(
useShortsPreference ? "revanced_remember_video_quality_toast_shorts" : "revanced_remember_video_quality_toast",
networkTypeMessage, (defaultQuality + "p")
));
}
/**
@ -62,9 +78,10 @@ public class RememberVideoQualityPatch {
*/
public static int setVideoQuality(Object[] qualities, final int originalQualityIndex, Object qInterface, String qIndexMethod) {
try {
boolean useShortsPreference = ShortsPlayerState.isOpen();
final int preferredQuality = Utils.getNetworkType() == NetworkType.MOBILE
? mobileQualitySetting.get()
: wifiQualitySetting.get();
? (useShortsPreference ? shortsQualityMobile : videoQualityMobile).get()
: (useShortsPreference ? shortsQualityWifi : videoQualityWifi).get();
if (!userChangedDefaultQuality && preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
return originalQualityIndex; // Nothing to do.
@ -141,17 +158,17 @@ public class RememberVideoQualityPatch {
* Injection point. Old quality menu.
*/
public static void userChangedQuality(int selectedQualityIndex) {
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get()) return;
userSelectedQualityIndex = selectedQualityIndex;
userChangedDefaultQuality = true;
if (shouldRememberVideoQuality()) {
userSelectedQualityIndex = selectedQualityIndex;
userChangedDefaultQuality = true;
}
}
/**
* Injection point. New quality menu.
*/
public static void userChangedQualityInNewFlyout(int selectedQuality) {
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get()) return;
if (!shouldRememberVideoQuality()) return;
changeDefaultQuality(selectedQuality); // Quality is human readable resolution (ie: 1080).
}

View File

@ -48,10 +48,13 @@ import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
public class Settings extends BaseSettings {
// Video
public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE);
public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", FALSE);
public static final IntegerSetting VIDEO_QUALITY_DEFAULT_WIFI = new IntegerSetting("revanced_video_quality_default_wifi", -2);
public static final IntegerSetting VIDEO_QUALITY_DEFAULT_MOBILE = new IntegerSetting("revanced_video_quality_default_mobile", -2);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", FALSE);
public static final IntegerSetting SHORTS_QUALITY_DEFAULT_WIFI = new IntegerSetting("revanced_shorts_quality_default_wifi", -2, true);
public static final IntegerSetting SHORTS_QUALITY_DEFAULT_MOBILE = new IntegerSetting("revanced_shorts_quality_default_mobile", -2, true);
public static final BooleanSetting REMEMBER_SHORTS_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_shorts_quality_last_selected", FALSE);
public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE);
// Speed
public static final FloatSetting SPEED_TAP_AND_HOLD = new FloatSetting("revanced_speed_tap_and_hold", 2.0f, true);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", FALSE);

View File

@ -5,7 +5,7 @@ import app.revanced.extension.youtube.Event
import app.revanced.extension.youtube.patches.VideoInformation
/**
* Main player type.
* Regular player type.
*/
enum class PlayerType {
/**
@ -90,8 +90,6 @@ enum class PlayerType {
* Does not include the first moment after a short is opened when a regular video is minimized on screen,
* or while watching a short with a regular video present on a spoofed 16.x version of YouTube.
* To include those situations instead use [isNoneHiddenOrMinimized].
*
* @see VideoInformation
*/
fun isNoneOrHidden(): Boolean {
return this == NONE || this == HIDDEN
@ -107,8 +105,11 @@ enum class PlayerType {
* when spoofing to an old version this will return false even
* though a Short is being opened or is on screen (see [isNoneHiddenOrMinimized]).
*
* Instead of this method, consider using {@link ShortsPlayerState}
* which may work better for some situations.
*
* @return If nothing, a Short, or a regular video is sliding off screen to a dismissed or hidden state.
* @see VideoInformation
* @see ShortsPlayerState
*/
fun isNoneHiddenOrSlidingMinimized(): Boolean {
return isNoneOrHidden() || this == WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED
@ -125,9 +126,12 @@ enum class PlayerType {
* Typically used to detect if a Short is playing when the player cannot be in a minimized state,
* such as the user interacting with a button or element of the player.
*
* Instead of this method, consider using {@link ShortsPlayerState}
* which may work better for some situations.
*
* @return If nothing, a Short, a regular video is sliding off screen to a dismissed or hidden state,
* a regular video is minimized (and a new video is not being opened).
* @see VideoInformation
* @see ShortsPlayerState
*/
fun isNoneHiddenOrMinimized(): Boolean {
return isNoneHiddenOrSlidingMinimized() || this == WATCH_WHILE_MINIMIZED

View File

@ -0,0 +1,38 @@
package app.revanced.extension.youtube.shared
import app.revanced.extension.shared.Logger
import app.revanced.extension.youtube.Event
/**
* Shorts player state.
*/
class ShortsPlayerState {
companion object {
@JvmStatic
fun setOpen(open: Boolean) {
if (isOpen != open) {
Logger.printDebug { "ShortsPlayerState open changed to: $isOpen" }
isOpen = open
onChange(open)
}
}
@Volatile
private var isOpen = false
/**
* Shorts player state change listener.
*/
@JvmStatic
val onChange = Event<Boolean>()
/**
* If the Shorts player is currently open.
*/
@JvmStatic
fun isOpen(): Boolean {
return isOpen
}
}
}