mirror of
https://github.com/ReVanced/revanced-patches.git
synced 2026-01-19 00:53:57 +00:00
feat(Prime Video): Add Playback speed patch (#5444)
This commit is contained in:
committed by
GitHub
parent
2136573cb6
commit
f46dbcd084
@@ -0,0 +1,207 @@
|
|||||||
|
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.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
|
import com.amazon.video.sdk.player.Player;
|
||||||
|
|
||||||
|
public class PlaybackSpeedPatch {
|
||||||
|
private static Player player;
|
||||||
|
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};
|
||||||
|
private static final String SPEED_BUTTON_TAG = "speed_overlay";
|
||||||
|
|
||||||
|
public static void setPlayer(Player playerInstance) {
|
||||||
|
player = playerInstance;
|
||||||
|
if (player != null) {
|
||||||
|
// Reset playback rate when switching between episodes to ensure correct display.
|
||||||
|
player.setPlaybackRate(1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void initializeSpeedOverlay(View userControlsView) {
|
||||||
|
try {
|
||||||
|
LinearLayout buttonContainer = Utils.getChildViewByResourceName(userControlsView, "ButtonContainerPlayerTop");
|
||||||
|
|
||||||
|
// If the speed overlay exists we should return early.
|
||||||
|
if (Utils.getChildView(buttonContainer, false, child ->
|
||||||
|
child instanceof ImageView && SPEED_BUTTON_TAG.equals(child.getTag())) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageView speedButton = createSpeedButton(userControlsView.getContext());
|
||||||
|
speedButton.setOnClickListener(v -> changePlaybackSpeed(speedButton));
|
||||||
|
buttonContainer.addView(speedButton, 0);
|
||||||
|
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Logger.printException(() -> "initializeSpeedOverlay, no button container found", e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.printException(() -> "initializeSpeedOverlay failure", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImageView createSpeedButton(Context context) {
|
||||||
|
ImageView speedButton = new ImageView(context);
|
||||||
|
speedButton.setContentDescription("Playback Speed");
|
||||||
|
speedButton.setTag(SPEED_BUTTON_TAG);
|
||||||
|
speedButton.setClickable(true);
|
||||||
|
speedButton.setFocusable(true);
|
||||||
|
speedButton.setScaleType(ImageView.ScaleType.CENTER);
|
||||||
|
|
||||||
|
SpeedIconDrawable speedIcon = new SpeedIconDrawable();
|
||||||
|
speedButton.setImageDrawable(speedIcon);
|
||||||
|
|
||||||
|
int buttonSize = Utils.dipToPixels(48);
|
||||||
|
speedButton.setMinimumWidth(buttonSize);
|
||||||
|
speedButton.setMinimumHeight(buttonSize);
|
||||||
|
|
||||||
|
return speedButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String[] getSpeedOptions() {
|
||||||
|
String[] options = new String[SPEED_VALUES.length];
|
||||||
|
for (int i = 0; i < SPEED_VALUES.length; i++) {
|
||||||
|
options[i] = SPEED_VALUES[i] + "x";
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void changePlaybackSpeed(ImageView imageView) {
|
||||||
|
if (player == null) {
|
||||||
|
Logger.printException(() -> "Player not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
player.pause();
|
||||||
|
AlertDialog dialog = createSpeedPlaybackDialog(imageView);
|
||||||
|
dialog.setOnDismissListener(dialogInterface -> player.play());
|
||||||
|
dialog.show();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.printException(() -> "changePlaybackSpeed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AlertDialog createSpeedPlaybackDialog(ImageView imageView) {
|
||||||
|
Context context = imageView.getContext();
|
||||||
|
int currentSelection = getCurrentSpeedSelection();
|
||||||
|
|
||||||
|
return new AlertDialog.Builder(context)
|
||||||
|
.setTitle("Select Playback Speed")
|
||||||
|
.setSingleChoiceItems(getSpeedOptions(), currentSelection,
|
||||||
|
PlaybackSpeedPatch::handleSpeedSelection)
|
||||||
|
.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getCurrentSpeedSelection() {
|
||||||
|
try {
|
||||||
|
float currentRate = player.getPlaybackRate();
|
||||||
|
int index = Arrays.binarySearch(SPEED_VALUES, currentRate);
|
||||||
|
return Math.max(index, 0); // Use slowest speed if not found.
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.printException(() -> "getCurrentSpeedSelection error getting current playback speed", e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleSpeedSelection(android.content.DialogInterface dialog, int selectedIndex) {
|
||||||
|
try {
|
||||||
|
float selectedSpeed = SPEED_VALUES[selectedIndex];
|
||||||
|
player.setPlaybackRate(selectedSpeed);
|
||||||
|
player.play();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.printException(() -> "handleSpeedSelection error setting playback speed", e);
|
||||||
|
} finally {
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
// Position gauge in lower portion.
|
||||||
|
float centerY = h * 0.7f;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicWidth() {
|
||||||
|
return Utils.dipToPixels(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicHeight() {
|
||||||
|
return Utils.dipToPixels(32);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,4 +4,10 @@ public interface VideoPlayer {
|
|||||||
long getCurrentPosition();
|
long getCurrentPosition();
|
||||||
|
|
||||||
void seekTo(long positionMs);
|
void seekTo(long positionMs);
|
||||||
|
|
||||||
|
void pause();
|
||||||
|
|
||||||
|
void play();
|
||||||
|
|
||||||
|
boolean isPlaying();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.amazon.video.sdk.player;
|
||||||
|
|
||||||
|
public interface Player {
|
||||||
|
float getPlaybackRate();
|
||||||
|
|
||||||
|
void setPlaybackRate(float rate);
|
||||||
|
|
||||||
|
void play();
|
||||||
|
|
||||||
|
void pause();
|
||||||
|
}
|
||||||
@@ -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 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 final class app/revanced/patches/protonmail/account/RemoveFreeAccountsLimitPatchKt {
|
||||||
public static final fun getRemoveFreeAccountsLimitPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
public static final fun getRemoveFreeAccountsLimitPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ val skipAdsPatch = bytecodePatch(
|
|||||||
name = "Skip ads",
|
name = "Skip ads",
|
||||||
description = "Automatically skips video stream ads.",
|
description = "Automatically skips video stream ads.",
|
||||||
) {
|
) {
|
||||||
compatibleWith("com.amazon.avod.thirdpartyclient"("3.0.403.257"))
|
compatibleWith("com.amazon.avod.thirdpartyclient"("3.0.412.2947"))
|
||||||
|
|
||||||
dependsOn(sharedExtensionPatch)
|
dependsOn(sharedExtensionPatch)
|
||||||
|
|
||||||
|
|||||||
@@ -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;"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
description = "Adds playback speed controls to the video player.",
|
||||||
|
) {
|
||||||
|
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->initializeSpeedOverlay(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
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user