1+ package app .revanced .extension .primevideo .videoplayer ;
2+
3+ import android .app .AlertDialog ;
4+ import android .content .Context ;
5+ import android .graphics .RectF ;
6+ import android .view .View ;
7+ import android .widget .ImageView ;
8+ import android .widget .LinearLayout ;
9+ import android .graphics .Color ;
10+ import android .graphics .drawable .Drawable ;
11+ import android .graphics .Canvas ;
12+ import android .graphics .Paint ;
13+ import android .graphics .ColorFilter ;
14+ import android .graphics .PixelFormat ;
15+ import java .util .Arrays ;
16+
17+ import app .revanced .extension .shared .Logger ;
18+ import app .revanced .extension .shared .Utils ;
19+
20+ import com .amazon .video .sdk .player .Player ;
21+
22+ public class PlaybackSpeedPatch {
23+ private static Player player ;
24+ private static final float [] SPEED_VALUES = {0.5f , 0.7f , 0.8f , 0.9f , 0.95f , 1.0f , 1.05f , 1.1f , 1.2f , 1.3f , 1.5f , 2.0f };
25+ private static final String SPEED_BUTTON_TAG = "speed_overlay" ;
26+
27+ public static void setPlayer (Player playerInstance ) {
28+ player = playerInstance ;
29+ if (player != null ) {
30+ // Reset playback rate when switching between episodes to ensure correct display.
31+ player .setPlaybackRate (1.0f );
32+ }
33+ }
34+
35+ public static void initializeSpeedOverlay (View userControlsView ) {
36+ try {
37+ LinearLayout buttonContainer = Utils .getChildViewByResourceName (userControlsView , "ButtonContainerPlayerTop" );
38+
39+ // If the speed overlay exists we should return early.
40+ if (Utils .getChildView (buttonContainer , false , child ->
41+ child instanceof ImageView && SPEED_BUTTON_TAG .equals (child .getTag ())) != null ) {
42+ return ;
43+ }
44+
45+ ImageView speedButton = createSpeedButton (userControlsView .getContext ());
46+ speedButton .setOnClickListener (v -> changePlaybackSpeed (speedButton ));
47+ buttonContainer .addView (speedButton , 0 );
48+
49+ } catch (IllegalArgumentException e ) {
50+ Logger .printException (() -> "initializeSpeedOverlay, no button container found" , e );
51+ } catch (Exception e ) {
52+ Logger .printException (() -> "initializeSpeedOverlay failure" , e );
53+ }
54+ }
55+
56+ private static ImageView createSpeedButton (Context context ) {
57+ ImageView speedButton = new ImageView (context );
58+ speedButton .setContentDescription ("Playback Speed" );
59+ speedButton .setTag (SPEED_BUTTON_TAG );
60+ speedButton .setClickable (true );
61+ speedButton .setFocusable (true );
62+ speedButton .setScaleType (ImageView .ScaleType .CENTER );
63+
64+ SpeedIconDrawable speedIcon = new SpeedIconDrawable ();
65+ speedButton .setImageDrawable (speedIcon );
66+
67+ int buttonSize = Utils .dipToPixels (48 );
68+ speedButton .setMinimumWidth (buttonSize );
69+ speedButton .setMinimumHeight (buttonSize );
70+
71+ return speedButton ;
72+ }
73+
74+ private static String [] getSpeedOptions () {
75+ String [] options = new String [SPEED_VALUES .length ];
76+ for (int i = 0 ; i < SPEED_VALUES .length ; i ++) {
77+ options [i ] = SPEED_VALUES [i ] + "x" ;
78+ }
79+ return options ;
80+ }
81+
82+ private static void changePlaybackSpeed (ImageView imageView ) {
83+ if (player == null ) {
84+ Logger .printException (() -> "Player not available" );
85+ return ;
86+ }
87+
88+ try {
89+ player .pause ();
90+ AlertDialog dialog = createSpeedPlaybackDialog (imageView );
91+ dialog .setOnDismissListener (dialogInterface -> player .play ());
92+ dialog .show ();
93+
94+ } catch (Exception e ) {
95+ Logger .printException (() -> "changePlaybackSpeed" , e );
96+ }
97+ }
98+
99+ private static AlertDialog createSpeedPlaybackDialog (ImageView imageView ) {
100+ Context context = imageView .getContext ();
101+ int currentSelection = getCurrentSpeedSelection ();
102+
103+ return new AlertDialog .Builder (context )
104+ .setTitle ("Select Playback Speed" )
105+ .setSingleChoiceItems (getSpeedOptions (), currentSelection ,
106+ PlaybackSpeedPatch ::handleSpeedSelection )
107+ .create ();
108+ }
109+
110+ private static int getCurrentSpeedSelection () {
111+ try {
112+ float currentRate = player .getPlaybackRate ();
113+ int index = Arrays .binarySearch (SPEED_VALUES , currentRate );
114+ return Math .max (index , 0 ); // Use slowest speed if not found.
115+ } catch (Exception e ) {
116+ Logger .printException (() -> "getCurrentSpeedSelection error getting current playback speed" , e );
117+ return 0 ;
118+ }
119+ }
120+
121+ private static void handleSpeedSelection (android .content .DialogInterface dialog , int selectedIndex ) {
122+ try {
123+ float selectedSpeed = SPEED_VALUES [selectedIndex ];
124+ player .setPlaybackRate (selectedSpeed );
125+ player .play ();
126+ } catch (Exception e ) {
127+ Logger .printException (() -> "handleSpeedSelection error setting playback speed" , e );
128+ } finally {
129+ dialog .dismiss ();
130+ }
131+ }
132+ }
133+
134+ class SpeedIconDrawable extends Drawable {
135+ private final Paint paint = new Paint (Paint .ANTI_ALIAS_FLAG );
136+
137+ @ Override
138+ public void draw (Canvas canvas ) {
139+ int w = getBounds ().width ();
140+ int h = getBounds ().height ();
141+ float centerX = w / 2f ;
142+ // Position gauge in lower portion.
143+ float centerY = h * 0.7f ;
144+ float radius = Math .min (w , h ) / 2f * 0.8f ;
145+
146+ paint .setColor (Color .WHITE );
147+ paint .setStyle (Paint .Style .STROKE );
148+ paint .setStrokeWidth (radius * 0.1f );
149+
150+ // Draw semicircle.
151+ RectF oval = new RectF (centerX - radius , centerY - radius , centerX + radius , centerY + radius );
152+ canvas .drawArc (oval , 180 , 180 , false , paint );
153+
154+ // Draw three tick marks.
155+ paint .setStrokeWidth (radius * 0.06f );
156+ for (int i = 0 ; i < 3 ; i ++) {
157+ float angle = 180 + (i * 45 ); // 180°, 225°, 270°.
158+ float angleRad = (float ) Math .toRadians (angle );
159+
160+ float startX = centerX + (radius * 0.8f ) * (float ) Math .cos (angleRad );
161+ float startY = centerY + (radius * 0.8f ) * (float ) Math .sin (angleRad );
162+ float endX = centerX + radius * (float ) Math .cos (angleRad );
163+ float endY = centerY + radius * (float ) Math .sin (angleRad );
164+
165+ canvas .drawLine (startX , startY , endX , endY , paint );
166+ }
167+
168+ // Draw needle.
169+ paint .setStrokeWidth (radius * 0.08f );
170+ float needleAngle = 200 ; // Slightly right of center.
171+ float needleAngleRad = (float ) Math .toRadians (needleAngle );
172+
173+ float needleEndX = centerX + (radius * 0.6f ) * (float ) Math .cos (needleAngleRad );
174+ float needleEndY = centerY + (radius * 0.6f ) * (float ) Math .sin (needleAngleRad );
175+
176+ canvas .drawLine (centerX , centerY , needleEndX , needleEndY , paint );
177+
178+ // Center dot.
179+ paint .setStyle (Paint .Style .FILL );
180+ canvas .drawCircle (centerX , centerY , radius * 0.06f , paint );
181+ }
182+
183+ @ Override
184+ public void setAlpha (int alpha ) {
185+ paint .setAlpha (alpha );
186+ }
187+
188+ @ Override
189+ public void setColorFilter (ColorFilter colorFilter ) {
190+ paint .setColorFilter (colorFilter );
191+ }
192+
193+ @ Override
194+ public int getOpacity () {
195+ return PixelFormat .TRANSLUCENT ;
196+ }
197+
198+ @ Override
199+ public int getIntrinsicWidth () {
200+ return Utils .dipToPixels (32 );
201+ }
202+
203+ @ Override
204+ public int getIntrinsicHeight () {
205+ return Utils .dipToPixels (32 );
206+ }
207+ }
0 commit comments