Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,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);
}
}
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
Expand Up @@ -12,7 +12,7 @@ val skipAdsPatch = bytecodePatch(
name = "Skip 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)

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",
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
"""
)
}
}
}
Loading