Skip to content

Commit 30176a3

Browse files
MarcaDianLisoUseInAIKyrios
andauthored
feat(YouTube - Playback speed): Show current playback speed on player speed dialog button (#5607)
Co-authored-by: LisoUseInAIKyrios <[email protected]>
1 parent 9c0638d commit 30176a3

File tree

38 files changed

+780
-774
lines changed

38 files changed

+780
-774
lines changed

extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ public static <R extends View> R getChildViewByResourceName(View view, String st
329329
return (R) child;
330330
}
331331

332-
throw new IllegalArgumentException("View with resource name '" + str + "' not found");
332+
throw new IllegalArgumentException("View with resource name not found: " + str);
333333
}
334334

335335
/**

extensions/youtube/src/main/java/app/revanced/extension/youtube/Event.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
package app.revanced.extension.youtube
22

3+
import app.revanced.extension.shared.Logger
4+
import java.util.Collections
5+
36
/**
47
* generic event provider class
58
*/
69
class Event<T> {
7-
private val eventListeners = mutableSetOf<(T) -> Unit>()
10+
private val eventListeners = Collections.synchronizedSet(mutableSetOf<(T) -> Unit>())
811

912
operator fun plusAssign(observer: (T) -> Unit) {
1013
addObserver(observer)
1114
}
1215

1316
fun addObserver(observer: (T) -> Unit) {
17+
Logger.printDebug { "Adding observer: $observer" }
1418
eventListeners.add(observer)
1519
}
1620

@@ -23,7 +27,8 @@ class Event<T> {
2327
}
2428

2529
operator fun invoke(value: T) {
26-
for (observer in eventListeners)
30+
for (observer in eventListeners) {
2731
observer.invoke(value)
32+
}
2833
}
2934
}

extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoInformation.java

Lines changed: 200 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package app.revanced.extension.youtube.patches;
22

33
import androidx.annotation.NonNull;
4+
import androidx.annotation.Nullable;
5+
6+
import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
47

58
import java.lang.ref.WeakReference;
9+
import java.util.Arrays;
610
import java.util.Objects;
711

812
import app.revanced.extension.shared.Logger;
913
import app.revanced.extension.shared.Utils;
14+
import app.revanced.extension.youtube.Event;
15+
import app.revanced.extension.youtube.shared.ShortsPlayerState;
1016
import app.revanced.extension.youtube.shared.VideoState;
1117

1218
/**
@@ -16,11 +22,31 @@
1622
public final class VideoInformation {
1723

1824
public interface PlaybackController {
19-
// Methods are added to YT classes during patching.
20-
boolean seekTo(long videoTime);
21-
void seekToRelative(long videoTimeOffset);
25+
// Methods are added during patching.
26+
boolean patch_seekTo(long videoTime);
27+
void patch_seekToRelative(long videoTimeOffset);
28+
}
29+
30+
/**
31+
* Interface to use obfuscated methods.
32+
*/
33+
public interface VideoQualityMenuInterface {
34+
// Method is added during patching.
35+
void patch_setQuality(VideoQuality quality);
2236
}
2337

38+
/**
39+
* Video resolution of the automatic quality option..
40+
*/
41+
public static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;
42+
43+
/**
44+
* All quality names are the same for all languages.
45+
* VideoQuality also has a resolution enum that can be used if needed.
46+
*/
47+
public static final String VIDEO_QUALITY_1080P_PREMIUM_NAME = "1080p Premium";
48+
49+
2450
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
2551
/**
2652
* Prefix present in all Short player parameters signature.
@@ -30,12 +56,10 @@ public interface PlaybackController {
3056
private static WeakReference<PlaybackController> playerControllerRef = new WeakReference<>(null);
3157
private static WeakReference<PlaybackController> mdxPlayerDirectorRef = new WeakReference<>(null);
3258

33-
@NonNull
3459
private static String videoId = "";
3560
private static long videoLength = 0;
3661
private static long videoTime = -1;
3762

38-
@NonNull
3963
private static volatile String playerResponseVideoId = "";
4064
private static volatile boolean playerResponseVideoIdIsShort;
4165
private static volatile boolean videoIdIsShort;
@@ -45,19 +69,63 @@ public interface PlaybackController {
4569
*/
4670
private static float playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
4771

72+
private static int desiredVideoResolution = AUTOMATIC_VIDEO_QUALITY_VALUE;
73+
74+
private static boolean qualityNeedsUpdating;
75+
76+
/**
77+
* The available qualities of the current video.
78+
*/
79+
@Nullable
80+
private static VideoQuality[] currentQualities;
81+
82+
/**
83+
* The current quality of the video playing.
84+
* This is always the actual quality even if Automatic quality is active.
85+
*/
86+
@Nullable
87+
private static VideoQuality currentQuality;
88+
89+
/**
90+
* The current VideoQualityMenuInterface, set during setVideoQuality.
91+
*/
92+
@Nullable
93+
private static VideoQualityMenuInterface currentMenuInterface;
94+
95+
/**
96+
* Callback for when the current quality changes.
97+
*/
98+
public static final Event<VideoQuality> onQualityChange = new Event<>();
99+
100+
@Nullable
101+
public static VideoQuality[] getCurrentQualities() {
102+
return currentQualities;
103+
}
104+
105+
@Nullable
106+
public static VideoQuality getCurrentQuality() {
107+
return currentQuality;
108+
}
109+
48110
/**
49111
* Injection point.
50112
*
51113
* @param playerController player controller object.
52114
*/
53115
public static void initialize(@NonNull PlaybackController playerController) {
54116
try {
117+
Logger.printDebug(() -> "newVideoStarted");
118+
55119
playerControllerRef = new WeakReference<>(Objects.requireNonNull(playerController));
56120
videoTime = -1;
57121
videoLength = 0;
58122
playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
123+
desiredVideoResolution = AUTOMATIC_VIDEO_QUALITY_VALUE;
124+
currentQualities = null;
125+
currentMenuInterface = null;
126+
setCurrentQuality(null);
59127
} catch (Exception ex) {
60-
Logger.printException(() -> "Failed to initialize", ex);
128+
Logger.printException(() -> "initialize failure", ex);
61129
}
62130
}
63131

@@ -197,14 +265,14 @@ public static boolean seekTo(final long seekTime) {
197265
if (controller == null) {
198266
Logger.printDebug(() -> "Cannot seekTo because player controller is null");
199267
} else {
200-
if (controller.seekTo(adjustedSeekTime)) return true;
268+
if (controller.patch_seekTo(adjustedSeekTime)) return true;
201269
Logger.printDebug(() -> "seekTo did not succeeded. Trying MXD.");
202270
// Else the video is loading or changing videos, or video is casting to a different device.
203271
}
204272

205273
// Try calling the seekTo method of the MDX player director (called when casting).
206274
// The difference has to be a different second mark in order to avoid infinite skip loops
207-
// as the Lounge API only supports seconds.
275+
// as the Lounge API only supports whole seconds.
208276
if (adjustedSeekTime / 1000 == videoTime / 1000) {
209277
Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small "
210278
+ "(" + (adjustedSeekTime - videoTime) + "ms)");
@@ -217,9 +285,9 @@ public static boolean seekTo(final long seekTime) {
217285
return false;
218286
}
219287

220-
return controller.seekTo(adjustedSeekTime);
288+
return controller.patch_seekTo(adjustedSeekTime);
221289
} catch (Exception ex) {
222-
Logger.printException(() -> "Failed to seek", ex);
290+
Logger.printException(() -> "seekTo failure", ex);
223291
return false;
224292
}
225293
}
@@ -239,7 +307,7 @@ public static void seekToRelative(long seekTime) {
239307
if (controller == null) {
240308
Logger.printDebug(() -> "Cannot seek relative as player controller is null");
241309
} else {
242-
controller.seekToRelative(seekTime);
310+
controller.patch_seekToRelative(seekTime);
243311
}
244312

245313
// Adjust the fine adjustment function so it's at least 1 second before/after.
@@ -255,10 +323,10 @@ public static void seekToRelative(long seekTime) {
255323
if (controller == null) {
256324
Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null");
257325
} else {
258-
controller.seekToRelative(adjustedSeekTime);
326+
controller.patch_seekToRelative(adjustedSeekTime);
259327
}
260328
} catch (Exception ex) {
261-
Logger.printException(() -> "Failed to seek relative", ex);
329+
Logger.printException(() -> "seekToRelative failure", ex);
262330
}
263331
}
264332

@@ -373,4 +441,123 @@ public static void setPlaybackSpeed(float newlyLoadedPlaybackSpeed) {
373441
playbackSpeed = newlyLoadedPlaybackSpeed;
374442
}
375443
}
444+
445+
/**
446+
* @param resolution The desired video quality resolution to use.
447+
*/
448+
public static void setDesiredVideoResolution(int resolution) {
449+
Utils.verifyOnMainThread();
450+
Logger.printDebug(() -> "Setting desired video resolution: " + resolution);
451+
desiredVideoResolution = resolution;
452+
qualityNeedsUpdating = true;
453+
}
454+
455+
private static void setCurrentQuality(@Nullable VideoQuality quality) {
456+
Utils.verifyOnMainThread();
457+
if (currentQuality != quality) {
458+
Logger.printDebug(() -> "Current quality changed to: " + quality);
459+
currentQuality = quality;
460+
onQualityChange.invoke(quality);
461+
}
462+
}
463+
464+
/**
465+
* Forcefully changes the video quality of the currently playing video.
466+
*/
467+
public static void changeQuality(VideoQuality quality) {
468+
Utils.verifyOnMainThread();
469+
470+
if (currentMenuInterface == null) {
471+
Logger.printException(() -> "Cannot change quality, menu interface is null");
472+
return;
473+
}
474+
currentMenuInterface.patch_setQuality(quality);
475+
}
476+
477+
/**
478+
* Injection point. Fixes bad data used by YouTube.
479+
*/
480+
public static int fixVideoQualityResolution(String name, int quality) {
481+
final int correctQuality = 480;
482+
if (name.equals("480p") && quality != correctQuality) {
483+
return correctQuality;
484+
}
485+
486+
return quality;
487+
}
488+
489+
/**
490+
* Injection point.
491+
*
492+
* @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2
493+
* @param originalQualityIndex quality index to use, as chosen by YouTube
494+
*/
495+
public static int setVideoQuality(VideoQuality[] qualities, VideoQualityMenuInterface menu, int originalQualityIndex) {
496+
try {
497+
Utils.verifyOnMainThread();
498+
currentMenuInterface = menu;
499+
500+
final boolean availableQualitiesChanged = (currentQualities == null)
501+
|| !Arrays.equals(currentQualities, qualities);
502+
if (availableQualitiesChanged) {
503+
currentQualities = qualities;
504+
Logger.printDebug(() -> "VideoQualities: " + Arrays.toString(currentQualities));
505+
}
506+
507+
VideoQuality updatedCurrentQuality = qualities[originalQualityIndex];
508+
if (updatedCurrentQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE
509+
&& (currentQuality == null || currentQuality != updatedCurrentQuality)) {
510+
setCurrentQuality(updatedCurrentQuality);
511+
}
512+
513+
final int preferredQuality = desiredVideoResolution;
514+
if (preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
515+
return originalQualityIndex; // Nothing to do.
516+
}
517+
518+
// After changing videos the qualities can initially be for the prior video.
519+
// If the qualities have changed and the default is not auto then an update is needed.
520+
if (qualityNeedsUpdating) {
521+
qualityNeedsUpdating = false;
522+
} else if (!availableQualitiesChanged) {
523+
return originalQualityIndex;
524+
}
525+
526+
// Find the highest quality that is equal to or less than the preferred.
527+
int i = 0;
528+
final int lastQualityIndex = qualities.length - 1;
529+
for (VideoQuality quality : qualities) {
530+
final int qualityResolution = quality.patch_getResolution();
531+
if ((qualityResolution != AUTOMATIC_VIDEO_QUALITY_VALUE && qualityResolution <= preferredQuality)
532+
// Use the lowest video quality if the default is lower than all available.
533+
|| i == lastQualityIndex) {
534+
final boolean qualityNeedsChange = (i != originalQualityIndex);
535+
Logger.printDebug(() -> qualityNeedsChange
536+
? "Changing video quality from: " + updatedCurrentQuality + " to: " + quality
537+
: "Video is already the preferred quality: " + quality
538+
);
539+
540+
// On first load of a new regular video, if the video is already the
541+
// desired quality then the quality flyout will show 'Auto' (ie: Auto (720p)).
542+
//
543+
// To prevent user confusion, set the video index even if the
544+
// quality is already correct so the UI picker will not display "Auto".
545+
//
546+
// Only change Shorts quality if the quality actually needs to change,
547+
// because the "auto" option is not shown in the flyout
548+
// and setting the same quality again can cause the Short to restart.
549+
if (qualityNeedsChange || !ShortsPlayerState.isOpen()) {
550+
changeQuality(quality);
551+
return i;
552+
}
553+
554+
return originalQualityIndex;
555+
}
556+
i++;
557+
}
558+
} catch (Exception ex) {
559+
Logger.printException(() -> "setVideoQuality failure", ex);
560+
}
561+
return originalQualityIndex;
562+
}
376563
}

0 commit comments

Comments
 (0)