- 
          
- 
                Notifications
    You must be signed in to change notification settings 
- Fork 604
          feat(Prime Video): Add Playback speed patch
          #5444
        
          New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
8d48fb7
              b919f23
              45d5eec
              e936ef5
              078f22d
              a334a44
              ba9e4c0
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
|  | ||
| speedButton.setTextColor(Color.WHITE); | ||
| speedButton.setClickable(true); | ||
| speedButton.setFocusable(true); | ||
|  | ||
| SpeedIconDrawable speedIcon = new SpeedIconDrawable(); | ||
| int iconSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32, | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| context.getResources().getDisplayMetrics()); | ||
| speedIcon.setBounds(0, 0, iconSize, iconSize); | ||
| speedButton.setCompoundDrawables(speedIcon, null, null, null); | ||
|         
                  LisoUseInAIKyrios marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
|  | ||
| 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); | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| speedButton.setLayoutParams(params); | ||
| speedButton.setOnClickListener(v -> changePlayBackSpeed(speedButton)); | ||
| int castButtonIndex = findCastButtonIndex(buttonContainer, context); | ||
| if (castButtonIndex != -1) { | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| 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")) { | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| return i; | ||
| } | ||
| } catch (Exception e) { | ||
| // Continue searching | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| } | ||
| } | ||
| } | ||
| return -1; | ||
| } | ||
|  | ||
| private static void changePlayBackSpeed(TextView speedText) { | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| 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); | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| } | ||
| } | ||
|  | ||
| 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}; | ||
|          | ||
|  | ||
| 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; | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| 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 | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| 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 | 
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.amazon.video.sdk.player; | ||
|  | ||
| public interface Player { | ||
| float getPlaybackRate(); | ||
|  | ||
| void setPlaybackRate(float rate); | ||
|  | ||
| void play(); | ||
|  | ||
| void pause(); | ||
| } | 
| 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", | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| description = "Adds playback speed control to the Prime Video player with a speed button in the player controls.", | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| ) { | ||
| dependsOn( | ||
| sharedExtensionPatch, | ||
| ) | ||
|  | ||
| compatibleWith( | ||
| "com.amazon.avod.thirdpartyclient"("3.0.412.2947") | ||
|         
                  therealsuji marked this conversation as resolved.
              Show resolved
            Hide resolved | ||
| ) | ||
|  | ||
| 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 | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| """ | ||
| ) | ||
| } | ||
|  | ||
| playbackUserControlsPrepareForPlaybackFingerprint.method.apply { | ||
| addInstructions( | ||
| 0, | ||
| """ | ||
| invoke-virtual {p1}, Lcom/amazon/avod/playbackclient/PlaybackContext;->getPlayer()Lcom/amazon/video/sdk/player/Player; | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| move-result-object v0 | ||
| invoke-static {v0}, $EXTENSION_CLASS_DESCRIPTOR->setPlayer(Lcom/amazon/video/sdk/player/Player;)V | ||
|         
                  therealsuji marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| """ | ||
| ) | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.