11package app .revanced .extension .youtube .patches ;
22
33import androidx .annotation .NonNull ;
4+ import androidx .annotation .Nullable ;
5+
6+ import com .google .android .libraries .youtube .innertube .model .media .VideoQuality ;
47
58import java .lang .ref .WeakReference ;
9+ import java .util .Arrays ;
610import java .util .Objects ;
711
812import app .revanced .extension .shared .Logger ;
913import app .revanced .extension .shared .Utils ;
14+ import app .revanced .extension .youtube .Event ;
15+ import app .revanced .extension .youtube .shared .ShortsPlayerState ;
1016import app .revanced .extension .youtube .shared .VideoState ;
1117
1218/**
1622public 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