Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
089191b
feat(YouTube): Add player button to change video quality
MarcaDian Jul 12, 2025
e6c184a
Merge remote-tracking branch 'upstream/dev' into icon-quality-button
MarcaDian Aug 1, 2025
5560caf
refactor
MarcaDian Aug 1, 2025
cf7e264
fix empty quality list when Auto is selected
MarcaDian Aug 1, 2025
e7ee115
apidump
LisoUseInAIKyrios Aug 1, 2025
a09977e
fix reset to Auto
MarcaDian Aug 1, 2025
d94040c
refactor
LisoUseInAIKyrios Aug 1, 2025
2c2d21c
Fix error if quality button is used before the video loads
LisoUseInAIKyrios Aug 1, 2025
6b0c01a
refactor
LisoUseInAIKyrios Aug 1, 2025
c067db4
refactor
LisoUseInAIKyrios Aug 1, 2025
0b020c1
Remove unneeded toast
LisoUseInAIKyrios Aug 1, 2025
e9198a6
Show error if resource is not found
LisoUseInAIKyrios Aug 1, 2025
f7e3d32
fix 2k resource
MarcaDian Aug 1, 2025
313d7d9
refactor
LisoUseInAIKyrios Aug 1, 2025
7e1946f
refactor
LisoUseInAIKyrios Aug 1, 2025
e1344bf
refactor
LisoUseInAIKyrios Aug 1, 2025
f754320
Show current video quality as button icon
LisoUseInAIKyrios Aug 1, 2025
3ceb34f
refactor. Button can still flicker when resuming a previously watched…
LisoUseInAIKyrios Aug 1, 2025
740a0ea
fix getAdjustedTitleForegroundColor
MarcaDian Aug 1, 2025
7c7b2f5
use debouncing (revert if it doesn't work)
MarcaDian Aug 1, 2025
3c5b7fa
Long tap to reset to default
LisoUseInAIKyrios Aug 1, 2025
194d13a
Move button dialog to button class
LisoUseInAIKyrios Aug 1, 2025
6adfaa9
rename drawable to unknown
LisoUseInAIKyrios Aug 1, 2025
24719f7
For now, ignore long press if default quality is automatic
LisoUseInAIKyrios Aug 1, 2025
aa1dd06
Open dialog if long press on automatic quality
LisoUseInAIKyrios Aug 1, 2025
946d9f5
Fix delayed button update after clicking
LisoUseInAIKyrios Aug 1, 2025
47455ba
Hide the quality icon until playback starts
LisoUseInAIKyrios Aug 1, 2025
2d0c61b
Remove delay logic that no longer seems needed
LisoUseInAIKyrios Aug 1, 2025
e874857
add 'Player buttons' submenu
MarcaDian Aug 1, 2025
35f920b
Merge remote-tracking branch 'origin/icon-quality-button' into icon-q…
MarcaDian Aug 1, 2025
c008b29
Adjust strings
LisoUseInAIKyrios Aug 1, 2025
35dbf51
Use haptic feedback on long press to open dialog
LisoUseInAIKyrios Aug 1, 2025
29ad1f8
Revert "add 'Player buttons' submenu"
LisoUseInAIKyrios Aug 1, 2025
563321a
refactor
LisoUseInAIKyrios Aug 1, 2025
da06d7a
Always show current resolution in flyoug and do not show "automatic"
LisoUseInAIKyrios Aug 1, 2025
9cb5bad
add 'LD', 'SD', 'FHD_PLUS' icons
MarcaDian Aug 2, 2025
8f6bf5c
add a shimmer effect to unknown icon state
MarcaDian Aug 2, 2025
1c24b39
refactor -> add a fade animations
MarcaDian Aug 2, 2025
4551046
Use enhanced bitrate icon
LisoUseInAIKyrios Aug 2, 2025
4330299
Revert "refactor -> add a fade animations"
MarcaDian Aug 2, 2025
802f809
fix 1080p Premium used when 1080p was requested
LisoUseInAIKyrios Aug 2, 2025
7cbfe8d
refactor
LisoUseInAIKyrios Aug 2, 2025
e9a24d6
Add delay to resolve flicking on rapid quality changes
LisoUseInAIKyrios Aug 2, 2025
10bb6ec
revert delayed set icon
LisoUseInAIKyrios Aug 2, 2025
917eb99
Second attempt to use a delay
LisoUseInAIKyrios Aug 2, 2025
921671c
Adjust delay
LisoUseInAIKyrios Aug 2, 2025
ed40711
Remove shimmer animation
LisoUseInAIKyrios Aug 2, 2025
b9e3cf1
revert second attempt to use a delay (icons can still be incorrect)
LisoUseInAIKyrios Aug 2, 2025
ef582bc
fix flickering?
LisoUseInAIKyrios Aug 2, 2025
496a46b
fix 1080p uses 1080p Premium?
LisoUseInAIKyrios Aug 2, 2025
902c088
change 'FHD_PLUS' icon
MarcaDian Aug 2, 2025
d1708e0
fix button showing the wrong icon if YT doesn't want to change from 1…
LisoUseInAIKyrios Aug 2, 2025
3f122d0
cleanup
LisoUseInAIKyrios Aug 2, 2025
53fcdb6
Fix wrong 1080p Premium icon if remember quality is enabled
LisoUseInAIKyrios Aug 2, 2025
f22f18e
don't show quality toast if saved quality did not change
LisoUseInAIKyrios Aug 2, 2025
d15427d
Cleanup, comments
LisoUseInAIKyrios Aug 2, 2025
1d49ade
Cleanup
LisoUseInAIKyrios Aug 2, 2025
7848fd3
Remove unused code
LisoUseInAIKyrios Aug 2, 2025
ebaa195
Fix Shorts stuttering/restarting on first load if a default quality i…
LisoUseInAIKyrios Aug 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1460,6 +1460,16 @@ public static int percentageWidthToPixels(int percentage) {
return (int) (metrics.widthPixels * (percentage / 100.0f));
}

/**
* Uses {@link #adjustColorBrightness(int, float)} depending if light or dark mode is active.
*/
@ColorInt
public static int adjustColorBrightness(@ColorInt int baseColor, float lightThemeFactor, float darkThemeFactor) {
return isDarkModeEnabled()
? adjustColorBrightness(baseColor, darkThemeFactor)
: adjustColorBrightness(baseColor, lightThemeFactor);
}

/**
* Adjusts the brightness of a color by lightening or darkening it based on the given factor.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
public final class AdvancedVideoQualityMenuPatch {

/**
* Injection point.
* Injection point. Regular videos.
*/
public static void onFlyoutMenuCreate(RecyclerView recyclerView) {
if (!Settings.ADVANCED_VIDEO_QUALITY_MENU.get()) return;
Expand Down Expand Up @@ -61,22 +61,12 @@ public static void onFlyoutMenuCreate(RecyclerView recyclerView) {
});
}


/**
* Injection point.
*
* Used to force the creation of the advanced menu item for the Shorts quality flyout.
*/
public static boolean forceAdvancedVideoQualityMenuCreation(boolean original) {
return Settings.ADVANCED_VIDEO_QUALITY_MENU.get() || original;
}

/**
* Injection point.
*
* Shorts video quality flyout.
*/
public static void showAdvancedVideoQualityMenu(ListView listView) {
public static void addVideoQualityListMenuListener(ListView listView) {
if (!Settings.ADVANCED_VIDEO_QUALITY_MENU.get()) return;

listView.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
Expand All @@ -91,7 +81,6 @@ public void onChildViewAdded(View parent, View child) {
listView.setSoundEffectsEnabled(false);
final var qualityItemMenuPosition = 4;
listView.performItemClick(null, qualityItemMenuPosition, 0);

} catch (Exception ex) {
Logger.printException(() -> "showAdvancedVideoQualityMenu failure", ex);
}
Expand All @@ -102,4 +91,13 @@ public void onChildViewRemoved(View parent, View child) {
}
});
}

/**
* Injection point.
*
* Used to force the creation of the advanced menu item for the Shorts quality flyout.
*/
public static boolean forceAdvancedVideoQualityMenuCreation(boolean original) {
return Settings.ADVANCED_VIDEO_QUALITY_MENU.get() || original;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
import app.revanced.extension.youtube.videoplayer.VideoQualityDialogButton;

@SuppressWarnings("unused")
public class RememberVideoQualityPatch {
Expand All @@ -25,10 +26,20 @@ public class RememberVideoQualityPatch {
* Interface to use obfuscated methods.
*/
public interface VideoQualityMenuInterface {
void patch_setMenuIndexFromQuality(VideoQuality quality);
void patch_setQuality(VideoQuality quality);
}

private static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;
/**
* Video resolution of the automatic quality option..
*/
public static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;

/**
* All quality names are the same for all languages.
* VideoQuality also has a resolution enum that can be used if needed.
*/
public static final String VIDEO_QUALITY_1080P_PREMIUM_NAME = "1080p Premium";

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;
Expand All @@ -37,31 +48,55 @@ public interface VideoQualityMenuInterface {
private static boolean qualityNeedsUpdating;

/**
* If the user selected a new quality from the flyout menu,
* and {@link Settings#REMEMBER_VIDEO_QUALITY_LAST_SELECTED}
* or {@link Settings#REMEMBER_SHORTS_QUALITY_LAST_SELECTED} is enabled.
* The available qualities of the current video.
*/
private static boolean userChangedDefaultQuality;
@Nullable
private static List<VideoQuality> currentQualities;

/**
* Index of the video quality chosen by the user from the flyout menu.
* The current quality of the video playing.
* This is always the actual quality even if Automatic quality is active.
*/
private static int userSelectedQualityIndex;
@Nullable
private static VideoQuality currentQuality;

/**
* The available qualities of the current video.
* The current VideoQualityMenuInterface, set during setVideoQuality.
*/
@Nullable
private static List<VideoQuality> videoQualities;
private static VideoQualityMenuInterface currentMenuInterface;

@Nullable
public static List<VideoQuality> getCurrentQualities() {
return currentQualities;
}

@Nullable
public static VideoQuality getCurrentQuality() {
return currentQuality;
}

private static boolean shouldRememberVideoQuality() {
BooleanSetting preference = ShortsPlayerState.isOpen() ?
Settings.REMEMBER_SHORTS_QUALITY_LAST_SELECTED
@Nullable
public static VideoQualityMenuInterface getCurrentMenuInterface() {
return currentMenuInterface;
}

public 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 qualityResolution) {
public static int getDefaultQualityResolution() {
final boolean isShorts = ShortsPlayerState.isOpen();
IntegerSetting preference = Utils.getNetworkType() == NetworkType.MOBILE
? (isShorts ? shortsQualityMobile : videoQualityMobile)
: (isShorts ? shortsQualityWifi : videoQualityWifi);
return preference.get();
}

public static void saveDefaultQuality(int qualityResolution) {
final boolean shortPlayerOpen = ShortsPlayerState.isOpen();
String networkTypeMessage;
IntegerSetting qualitySetting;
Expand All @@ -72,16 +107,24 @@ private static void changeDefaultQuality(int qualityResolution) {
networkTypeMessage = str("revanced_remember_video_quality_wifi");
qualitySetting = shortPlayerOpen ? shortsQualityWifi : videoQualityWifi;
}

if (qualitySetting.get() == qualityResolution) {
// User clicked the same video quality as the current video,
// or changed between 1080p Premium and non-Premium.
return;
}
qualitySetting.save(qualityResolution);

if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get())
if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get()) {
String qualityLabel = qualityResolution + "p";
Utils.showToastShort(str(
shortPlayerOpen
? "revanced_remember_video_quality_toast_shorts"
: "revanced_remember_video_quality_toast",
networkTypeMessage,
(qualityResolution + "p"))
qualityLabel)
);
}
}

/**
Expand All @@ -93,111 +136,101 @@ private static void changeDefaultQuality(int qualityResolution) {
public static int setVideoQuality(VideoQuality[] qualities, VideoQualityMenuInterface menu, int originalQualityIndex) {
try {
Utils.verifyOnMainThread();
currentMenuInterface = menu;

final boolean useShortsPreference = ShortsPlayerState.isOpen();
final int preferredQuality = Utils.getNetworkType() == NetworkType.MOBILE
? (useShortsPreference ? shortsQualityMobile : videoQualityMobile).get()
: (useShortsPreference ? shortsQualityWifi : videoQualityWifi).get();

if (!userChangedDefaultQuality && preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
return originalQualityIndex; // Nothing to do.
final boolean availableQualitiesChanged = currentQualities == null
|| currentQualities.size() != qualities.length;
if (availableQualitiesChanged) {
currentQualities = Arrays.asList(qualities);
Logger.printDebug(() -> "VideoQualities: " + currentQualities);
}

if (videoQualities == null || videoQualities.size() != qualities.length) {
videoQualities = Arrays.asList(qualities);
VideoQuality updatedCurrentQuality = qualities[originalQualityIndex];
if (updatedCurrentQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE &&
(currentQuality == null
|| !currentQuality.patch_getQualityName().equals(updatedCurrentQuality.patch_getQualityName()))) {
currentQuality = updatedCurrentQuality;
Logger.printDebug(() -> "Current quality changed to: " + updatedCurrentQuality);

// After changing videos the qualities can initially be for the prior video.
// So if the qualities have changed an update is needed.
qualityNeedsUpdating = true;
Logger.printDebug(() -> "VideoQualities: " + videoQualities);
VideoQualityDialogButton.updateButtonIcon(updatedCurrentQuality);
}

if (userChangedDefaultQuality) {
userChangedDefaultQuality = false;
VideoQuality quality = videoQualities.get(userSelectedQualityIndex);
Logger.printDebug(() -> "User changed default quality to: " + quality);
changeDefaultQuality(quality.patch_getResolution());
return userSelectedQualityIndex;
final int preferredQuality = getDefaultQualityResolution();
if (preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
return originalQualityIndex; // Nothing to do.
}

if (!qualityNeedsUpdating) {
// After changing videos the qualities can initially be for the prior video.
// If the qualities have changed and the default is not auto then an update is needed.
if (!qualityNeedsUpdating && !availableQualitiesChanged) {
return originalQualityIndex;
}
qualityNeedsUpdating = false;

// Find the highest quality that is equal to or less than the preferred.
VideoQuality qualityToUse = videoQualities.get(0); // First element is automatic mode.
int qualityIndexToUse = 0;
int i = 0;
for (VideoQuality quality : videoQualities) {
for (VideoQuality quality : qualities) {
final int qualityResolution = quality.patch_getResolution();
if (qualityResolution > qualityToUse.patch_getResolution() && qualityResolution <= preferredQuality) {
qualityToUse = quality;
qualityIndexToUse = i;
break;
if (qualityResolution != AUTOMATIC_VIDEO_QUALITY_VALUE && qualityResolution <= preferredQuality) {
final boolean qualityNeedsChange = (i != originalQualityIndex);
Logger.printDebug(() -> qualityNeedsChange
? "Changing video quality from: " + updatedCurrentQuality + " to: " + quality
: "Video is already the preferred quality: " + quality
);

// On first load of a new regular video, if the video is already the
// desired quality then the quality flyout will show 'Auto' (ie: Auto (720p)).
//
// To prevent user confusion, set the video index even if the
// quality is already correct so the UI picker will not display "Auto".
//
// Only change Shorts quality if the quality actually needs to change,
// because the "auto" option is not shown in the flyout
// and setting the same quality again can cause the Short to restart.
if (qualityNeedsChange || !ShortsPlayerState.isOpen()) {
menu.patch_setQuality(qualities[i]);
return i;
}

return originalQualityIndex;
}
i++;
}

// If the desired quality index is equal to the original index,
// then the video is already set to the desired default quality.
String qualityToUseName = qualityToUse.patch_getQualityName();
if (qualityIndexToUse == originalQualityIndex) {
Logger.printDebug(() -> "Video is already preferred quality: " + qualityToUseName);
} else {
Logger.printDebug(() -> "Changing video quality from: "
+ videoQualities.get(originalQualityIndex).patch_getQualityName()
+ " to: " + qualityToUseName);
}

// On first load of a new video, if the video is already the desired quality
// then the quality flyout will show 'Auto' (ie: Auto (720p)).
//
// To prevent user confusion, set the video index even if the
// quality is already correct so the UI picker will not display "Auto".
menu.patch_setMenuIndexFromQuality(qualities[qualityIndexToUse]);

return qualityIndexToUse;
} catch (Exception ex) {
Logger.printException(() -> "setVideoQuality failure", ex);
return originalQualityIndex;
}
}

/**
* Injection point. Fixes bad data used by YouTube.
*/
public static int fixVideoQualityResolution(String name, int quality) {
final int correctQuality = 480;
if (name.equals("480p") && quality != correctQuality) {
Logger.printDebug(() -> "Fixing bad data of " + name + " from: " + quality
+ " to: " + correctQuality);
return correctQuality;
}

return quality;
return originalQualityIndex;
}

/**
* Injection point.
* @param qualityIndex Element index of {@link #videoQualities}.
* @param userSelectedQualityIndex Element index of {@link #currentQualities}.
*/
public static void userChangedQuality(int qualityIndex) {
if (shouldRememberVideoQuality()) {
userSelectedQualityIndex = qualityIndex;
userChangedDefaultQuality = true;
public static void userChangedShortsQuality(int userSelectedQualityIndex) {
try {
if (shouldRememberVideoQuality()) {
if (currentQualities == null) {
Logger.printDebug(() -> "Cannot save default quality, qualities is null");
return;
}
VideoQuality quality = currentQualities.get(userSelectedQualityIndex);
saveDefaultQuality(quality.patch_getResolution());
}
} catch (Exception ex) {
Logger.printException(() -> "userChangedShortsQuality failure", ex);
}
}

/**
* Injection point.
* Injection point. Regular videos.
* @param videoResolution Human readable resolution: 480, 720, 1080.
*/
public static void userChangedQualityInFlyout(int videoResolution) {
public static void userChangedQuality(int videoResolution) {
Utils.verifyOnMainThread();
if (!shouldRememberVideoQuality()) return;

changeDefaultQuality(videoResolution);
if (shouldRememberVideoQuality()) {
saveDefaultQuality(videoResolution);
}
}

/**
Expand All @@ -207,7 +240,24 @@ public static void newVideoStarted(VideoInformation.PlaybackController ignoredPl
Utils.verifyOnMainThread();

Logger.printDebug(() -> "newVideoStarted");
currentQualities = null;
currentQuality = null;
currentMenuInterface = null;
qualityNeedsUpdating = true;
videoQualities = null;

// Hide the quality button until playback starts and the qualities are available.
VideoQualityDialogButton.updateButtonIcon(null);
}

/**
* Injection point. Fixes bad data used by YouTube.
*/
public static int fixVideoQualityResolution(String name, int quality) {
final int correctQuality = 480;
if (name.equals("480p") && quality != correctQuality) {
return correctQuality;
}

return quality;
}
}
Loading
Loading