Skip to content

Commit f46dbcd

Browse files
authored
feat(Prime Video): Add Playback speed patch (#5444)
1 parent 2136573 commit f46dbcd

File tree

7 files changed

+308
-1
lines changed

7 files changed

+308
-1
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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+
}

extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/VideoPlayer.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,10 @@ public interface VideoPlayer {
44
long getCurrentPosition();
55

66
void seekTo(long positionMs);
7+
8+
void pause();
9+
10+
void play();
11+
12+
boolean isPlaying();
713
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.amazon.video.sdk.player;
2+
3+
public interface Player {
4+
float getPlaybackRate();
5+
6+
void setPlaybackRate(float rate);
7+
8+
void play();
9+
10+
void pause();
11+
}

patches/api/patches.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,10 @@ public final class app/revanced/patches/primevideo/misc/permissions/RenamePermis
476476
public static final fun getRenamePermissionsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
477477
}
478478

479+
public final class app/revanced/patches/primevideo/video/speed/PlaybackSpeedPatchKt {
480+
public static final fun getPlaybackSpeedPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
481+
}
482+
479483
public final class app/revanced/patches/protonmail/account/RemoveFreeAccountsLimitPatchKt {
480484
public static final fun getRemoveFreeAccountsLimitPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
481485
}

patches/src/main/kotlin/app/revanced/patches/primevideo/ads/SkipAdsPatch.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ val skipAdsPatch = bytecodePatch(
1212
name = "Skip ads",
1313
description = "Automatically skips video stream ads.",
1414
) {
15-
compatibleWith("com.amazon.avod.thirdpartyclient"("3.0.403.257"))
15+
compatibleWith("com.amazon.avod.thirdpartyclient"("3.0.412.2947"))
1616

1717
dependsOn(sharedExtensionPatch)
1818

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package app.revanced.patches.primevideo.video.speed
2+
3+
import app.revanced.patcher.fingerprint
4+
import com.android.tools.smali.dexlib2.AccessFlags
5+
6+
internal val playbackUserControlsInitializeFingerprint = fingerprint {
7+
accessFlags(AccessFlags.PUBLIC)
8+
parameters("Lcom/amazon/avod/playbackclient/PlaybackInitializationContext;")
9+
returns("V")
10+
custom { method, classDef ->
11+
method.name == "initialize" && classDef.type == "Lcom/amazon/avod/playbackclient/activity/feature/PlaybackUserControlsFeature;"
12+
}
13+
}
14+
15+
internal val playbackUserControlsPrepareForPlaybackFingerprint = fingerprint {
16+
accessFlags(AccessFlags.PUBLIC)
17+
parameters("Lcom/amazon/avod/playbackclient/PlaybackContext;")
18+
returns("V")
19+
custom { method, classDef ->
20+
method.name == "prepareForPlayback" &&
21+
classDef.type == "Lcom/amazon/avod/playbackclient/activity/feature/PlaybackUserControlsFeature;"
22+
}
23+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package app.revanced.patches.primevideo.video.speed
2+
3+
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
4+
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
5+
import app.revanced.patcher.patch.bytecodePatch
6+
import app.revanced.patches.primevideo.misc.extension.sharedExtensionPatch
7+
import app.revanced.util.getReference
8+
import app.revanced.util.indexOfFirstInstructionOrThrow
9+
import com.android.tools.smali.dexlib2.Opcode
10+
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
11+
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
12+
13+
private const val EXTENSION_CLASS_DESCRIPTOR =
14+
"Lapp/revanced/extension/primevideo/videoplayer/PlaybackSpeedPatch;"
15+
16+
val playbackSpeedPatch = bytecodePatch(
17+
name = "Playback speed",
18+
description = "Adds playback speed controls to the video player.",
19+
) {
20+
dependsOn(
21+
sharedExtensionPatch,
22+
)
23+
24+
compatibleWith(
25+
"com.amazon.avod.thirdpartyclient"("3.0.412.2947")
26+
)
27+
28+
execute {
29+
playbackUserControlsInitializeFingerprint.method.apply {
30+
val getIndex = indexOfFirstInstructionOrThrow {
31+
opcode == Opcode.IPUT_OBJECT &&
32+
getReference<FieldReference>()?.name == "mUserControls"
33+
}
34+
35+
val getRegister = getInstruction<OneRegisterInstruction>(getIndex).registerA
36+
37+
addInstructions(
38+
getIndex + 1,
39+
"""
40+
invoke-static { v$getRegister }, $EXTENSION_CLASS_DESCRIPTOR->initializeSpeedOverlay(Landroid/view/View;)V
41+
"""
42+
)
43+
}
44+
45+
playbackUserControlsPrepareForPlaybackFingerprint.method.apply {
46+
addInstructions(
47+
0,
48+
"""
49+
invoke-virtual { p1 }, Lcom/amazon/avod/playbackclient/PlaybackContext;->getPlayer()Lcom/amazon/video/sdk/player/Player;
50+
move-result-object v0
51+
invoke-static { v0 }, $EXTENSION_CLASS_DESCRIPTOR->setPlayer(Lcom/amazon/video/sdk/player/Player;)V
52+
"""
53+
)
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)