Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
@@ -0,0 +1,257 @@
package app.revanced.extension.primevideo.videoplayer;

import android.app.AlertDialog;
import android.content.Context;
import android.graphics.RectF;
import android.view.View;
import android.widget.TextView;
import android.widget.LinearLayout;
import android.view.ViewGroup;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import android.view.Gravity;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.ColorFilter;
import android.graphics.PixelFormat;

import app.revanced.extension.shared.Logger;

import com.amazon.video.sdk.player.Player;

public class PlaybackSpeedPatch {
private static Player player;


public static void setPlayer(Player playerInstance) {
player = playerInstance;
}

public static void initializeTextOverlay(View userControlsView) {
try {
LinearLayout buttonContainer = findTopButtonContainer(userControlsView);
if (buttonContainer == null) {
return;
}

for (int i = 0; i < buttonContainer.getChildCount(); i++) {
View child = buttonContainer.getChildAt(i);
if (child instanceof TextView && child.getTag() != null && "speed_overlay".equals(child.getTag())) {
return;
}
}

Context context = userControlsView.getContext();
TextView speedButton = new TextView(context);
speedButton.setTag("speed_overlay");
speedButton.setText("");
speedButton.setGravity(Gravity.CENTER_VERTICAL);

speedButton.setTextColor(Color.WHITE);
speedButton.setClickable(true);
speedButton.setFocusable(true);

SpeedIconDrawable speedIcon = new SpeedIconDrawable();
int iconSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32,
context.getResources().getDisplayMetrics());
speedIcon.setBounds(0, 0, iconSize, iconSize);
speedButton.setCompoundDrawables(speedIcon, null, null, null);

int buttonSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
context.getResources().getDisplayMetrics());
speedButton.setMinimumWidth(buttonSize);
speedButton.setMinimumHeight(buttonSize);

LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.setMargins(0, 0, 4, 0);
speedButton.setLayoutParams(params);
speedButton.setOnClickListener(v -> changePlayBackSpeed(speedButton));
int castButtonIndex = findCastButtonIndex(buttonContainer, context);
if (castButtonIndex != -1) {
buttonContainer.addView(speedButton, castButtonIndex);
} else {
buttonContainer.addView(speedButton);
}

} catch (Exception e) {
Logger.printException(() -> "Error initializing speed overlay", e);
}
}

private static int findCastButtonIndex(LinearLayout buttonContainer, Context context) {
for (int i = 0; i < buttonContainer.getChildCount(); i++) {
View child = buttonContainer.getChildAt(i);
if (child.getId() != View.NO_ID) {
try {
String resourceName = context.getResources().getResourceEntryName(child.getId());
if (resourceName != null && resourceName.equals("player_cast_btn")) {
return i;
}
} catch (Exception e) {
// Continue searching
}
}
}
return -1;
}

private static void changePlayBackSpeed(TextView speedText) {
try {
if (player != null) {
player.pause();

AlertDialog dialog = speedPlaybackDialog(speedText);
dialog.setOnDismissListener(dialogInterface -> {
try {
if (player != null) {
player.play();
}
} catch (Exception e) {
Logger.printException(() -> "Error resuming playback", e);
}
});

dialog.show();
} else {
Logger.printDebug(() -> "Player not available");
}
} catch (Exception e) {
Logger.printException(() -> "Error in changePlayBackSpeed", e);
}
}

private static AlertDialog speedPlaybackDialog(TextView speedText) {
Context context = speedText.getContext();
String[] speedOptions = {"1.0x", "1.5x", "2.0x"};
float[] speedValues = {1.0f, 1.5f, 2.0f};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work with other speeds?

0.9/1.1 are really helpful if they can be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The player has a min of 0.5x and 2x
Can you suggest some speed values to use ?

Copy link
Contributor

@LisoUseInAIKyrios LisoUseInAIKyrios Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally this might have -/+ buttons that change the current speed by 0.05x like YouTube, but thats more work than providing a lot of preset speeds.

Speeds near 1.0x are great to speed up or slow down just a little (especially slowing down foreign videos for non native speakers).

How many presets can fit? If there's room then can put a lot of them:
0.5, 0.7, 0.8, 0.9, 0.95, 1.0, 1.05, 1.1, 1.2, 1.3, 1.5, 2.0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the following
"0.5f, 0.7f, 0.8f, 0.9f, 0.95f, 1.0f, 1.05f, 1.1f, 1.2f, 1.3f, 1.5f, 2.0f"
Dialog options list needs to be scrolled, so can fit all


int currentSelection = 0;
if (player != null) {
try {
float currentRate = player.getPlaybackRate();
for (int i = 0; i < speedValues.length; i++) {
if (Math.abs(currentRate - speedValues[i]) < 0.1f) {
currentSelection = i;
break;
}
}
} catch (Exception e) {
Logger.printException(() -> "Error getting current playback rate", e);
}
}

AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("Select Playback Speed");
builder.setSingleChoiceItems(speedOptions, currentSelection, (dialog, which) -> {
try {
if (player != null) {
float speed = speedValues[which];
player.setPlaybackRate(speed);
player.play();
}
} catch (Exception e) {
Logger.printException(() -> "Error setting playback speed", e);
}
dialog.dismiss();
});

return builder.create();
}

private static LinearLayout findTopButtonContainer(View userControlsView) {
try {
if (userControlsView instanceof ViewGroup viewGroup) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);

if (child instanceof LinearLayout && child.getId() != View.NO_ID) {
try {
String resourceName = userControlsView.getContext().getResources().getResourceEntryName(child.getId());
if (resourceName != null && resourceName.equals("ButtonContainerPlayerTop")) {
return (LinearLayout) child;
}
} catch (Exception e) {
Logger.printException(() -> "Error finding button container", e);
}
}

LinearLayout result = findTopButtonContainer(child);
if (result != null) {
return result;
}
}
}
} catch (Exception e) {
Logger.printException(() -> "Error finding button container", e);
}
return null;
}
}

class SpeedIconDrawable extends Drawable {
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

@Override
public void draw(Canvas canvas) {
int w = getBounds().width();
int h = getBounds().height();
float centerX = w / 2f;
float centerY = h * 0.7f; // Position gauge in lower portion
float radius = Math.min(w, h) / 2f * 0.8f;

paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(radius * 0.1f);

// Draw semicircle
RectF oval = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
canvas.drawArc(oval, 180, 180, false, paint);

// Draw three tick marks
paint.setStrokeWidth(radius * 0.06f);
for (int i = 0; i < 3; i++) {
float angle = 180 + (i * 45); // 180°, 225°, 270°
float angleRad = (float) Math.toRadians(angle);

float startX = centerX + (radius * 0.8f) * (float) Math.cos(angleRad);
float startY = centerY + (radius * 0.8f) * (float) Math.sin(angleRad);
float endX = centerX + radius * (float) Math.cos(angleRad);
float endY = centerY + radius * (float) Math.sin(angleRad);

canvas.drawLine(startX, startY, endX, endY, paint);
}

// Draw needle
paint.setStrokeWidth(radius * 0.08f);
float needleAngle = 200; // Slightly right of center
float needleAngleRad = (float) Math.toRadians(needleAngle);

float needleEndX = centerX + (radius * 0.6f) * (float) Math.cos(needleAngleRad);
float needleEndY = centerY + (radius * 0.6f) * (float) Math.sin(needleAngleRad);

canvas.drawLine(centerX, centerY, needleEndX, needleEndY, paint);

// Center dot
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(centerX, centerY, radius * 0.06f, paint);
}

@Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
}

@Override
public void setColorFilter(ColorFilter colorFilter) {
paint.setColorFilter(colorFilter);
}

@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ public interface VideoPlayer {
long getCurrentPosition();

void seekTo(long positionMs);

void pause();

void play();

boolean isPlaying();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.amazon.video.sdk.player;

public interface Player {
float getPlaybackRate();

void setPlaybackRate(float rate);

void play();

void pause();
}
4 changes: 4 additions & 0 deletions patches/api/patches.api
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,10 @@ public final class app/revanced/patches/primevideo/misc/permissions/RenamePermis
public static final fun getRenamePermissionsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}

public final class app/revanced/patches/primevideo/video/speed/PlaybackSpeedPatchKt {
public static final fun getPlaybackSpeedPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}

public final class app/revanced/patches/protonmail/account/RemoveFreeAccountsLimitPatchKt {
public static final fun getRemoveFreeAccountsLimitPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package app.revanced.patches.primevideo.video.speed

import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags

internal val playbackUserControlsInitializeFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC)
parameters("Lcom/amazon/avod/playbackclient/PlaybackInitializationContext;")
returns("V")
custom { method, classDef ->
method.name == "initialize" && classDef.type == "Lcom/amazon/avod/playbackclient/activity/feature/PlaybackUserControlsFeature;"
}
}

internal val playbackUserControlsPrepareForPlaybackFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC)
parameters("Lcom/amazon/avod/playbackclient/PlaybackContext;")
returns("V")
custom { method, classDef ->
method.name == "prepareForPlayback" &&
classDef.type == "Lcom/amazon/avod/playbackclient/activity/feature/PlaybackUserControlsFeature;"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package app.revanced.patches.primevideo.video.speed

import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.primevideo.misc.extension.sharedExtensionPatch
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference

private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/primevideo/videoplayer/PlaybackSpeedPatch;"

val playbackSpeedPatch = bytecodePatch(
name = "Playback speed control",
description = "Adds playback speed control to the Prime Video player with a speed button in the player controls.",
) {
dependsOn(
sharedExtensionPatch,
)

compatibleWith(
"com.amazon.avod.thirdpartyclient"("3.0.412.2947")
)

execute {
playbackUserControlsInitializeFingerprint.method.apply {
val getIndex = indexOfFirstInstructionOrThrow {
opcode == Opcode.IPUT_OBJECT &&
getReference<FieldReference>()?.name == "mUserControls"
}

val getRegister = getInstruction<OneRegisterInstruction>(getIndex).registerA

addInstructions(
getIndex + 1,
"""
invoke-static {v$getRegister}, $EXTENSION_CLASS_DESCRIPTOR->initializeTextOverlay(Landroid/view/View;)V
"""
)
}

playbackUserControlsPrepareForPlaybackFingerprint.method.apply {
addInstructions(
0,
"""
invoke-virtual {p1}, Lcom/amazon/avod/playbackclient/PlaybackContext;->getPlayer()Lcom/amazon/video/sdk/player/Player;
move-result-object v0
invoke-static {v0}, $EXTENSION_CLASS_DESCRIPTOR->setPlayer(Lcom/amazon/video/sdk/player/Player;)V
"""
)
}
}
}
Loading