diff --git a/extensions/primevideo/build.gradle.kts b/extensions/primevideo/build.gradle.kts
new file mode 100644
index 000000000..9a81cc3e8
--- /dev/null
+++ b/extensions/primevideo/build.gradle.kts
@@ -0,0 +1,4 @@
+dependencies {
+ compileOnly(project(":extensions:shared:library"))
+ compileOnly(project(":extensions:primevideo:stub"))
+}
diff --git a/extensions/primevideo/src/main/AndroidManifest.xml b/extensions/primevideo/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9b65eb06c
--- /dev/null
+++ b/extensions/primevideo/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/ads/SkipAdsPatch.java b/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/ads/SkipAdsPatch.java
new file mode 100644
index 000000000..d0a97810a
--- /dev/null
+++ b/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/ads/SkipAdsPatch.java
@@ -0,0 +1,36 @@
+package app.revanced.extension.primevideo.ads;
+
+import com.amazon.avod.fsm.SimpleTrigger;
+import com.amazon.avod.media.ads.AdBreak;
+import com.amazon.avod.media.ads.internal.state.AdBreakTrigger;
+import com.amazon.avod.media.ads.internal.state.AdEnabledPlayerTriggerType;
+import com.amazon.avod.media.playback.VideoPlayer;
+import com.amazon.avod.media.ads.internal.state.ServerInsertedAdBreakState;
+
+import app.revanced.extension.shared.Logger;
+
+@SuppressWarnings("unused")
+public final class SkipAdsPatch {
+ public static void enterServerInsertedAdBreakState(ServerInsertedAdBreakState state, AdBreakTrigger trigger, VideoPlayer player) {
+ try {
+ AdBreak adBreak = trigger.getBreak();
+
+ // There are two scenarios when entering the original method:
+ // 1. Player naturally entered an ad break while watching a video.
+ // 2. User is skipped/scrubbed to a position on the timeline. If seek position is past an ad break,
+ // user is forced to watch an ad before continuing.
+ //
+ // Scenario 2 is indicated by trigger.getSeekStartPosition() != null, so skip directly to the scrubbing
+ // target. Otherwise, just calculate when the ad break should end and skip to there.
+ if (trigger.getSeekStartPosition() != null)
+ player.seekTo(trigger.getSeekTarget().getTotalMilliseconds());
+ else
+ player.seekTo(player.getCurrentPosition() + adBreak.getDurationExcludingAux().getTotalMilliseconds());
+
+ // Send "end of ads" trigger to state machine so everything doesn't get whacky.
+ state.doTrigger(new SimpleTrigger(AdEnabledPlayerTriggerType.NO_MORE_ADS_SKIP_TRANSITION));
+ } catch (Exception ex) {
+ Logger.printException(() -> "Failed skipping ads", ex);
+ }
+ }
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/build.gradle.kts b/extensions/primevideo/stub/build.gradle.kts
new file mode 100644
index 000000000..2d9865785
--- /dev/null
+++ b/extensions/primevideo/stub/build.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+ id(libs.plugins.android.library.get().pluginId)
+}
+
+android {
+ namespace = "app.revanced.extension"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 21
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+}
diff --git a/extensions/primevideo/stub/src/main/AndroidManifest.xml b/extensions/primevideo/stub/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9b65eb06c
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/SimpleTrigger.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/SimpleTrigger.java
new file mode 100644
index 000000000..b537fe040
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/SimpleTrigger.java
@@ -0,0 +1,6 @@
+package com.amazon.avod.fsm;
+
+public final class SimpleTrigger implements Trigger {
+ public SimpleTrigger(T triggerType) {
+ }
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/StateBase.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/StateBase.java
new file mode 100644
index 000000000..95741308c
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/StateBase.java
@@ -0,0 +1,7 @@
+package com.amazon.avod.fsm;
+
+public abstract class StateBase {
+ // This method orginally has protected access (modified in patch code).
+ public void doTrigger(Trigger trigger) {
+ }
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/Trigger.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/Trigger.java
new file mode 100644
index 000000000..282f0f200
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/Trigger.java
@@ -0,0 +1,4 @@
+package com.amazon.avod.fsm;
+
+public interface Trigger {
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/TimeSpan.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/TimeSpan.java
new file mode 100644
index 000000000..cc90e43cd
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/TimeSpan.java
@@ -0,0 +1,7 @@
+package com.amazon.avod.media;
+
+public final class TimeSpan {
+ public long getTotalMilliseconds() {
+ throw new UnsupportedOperationException();
+ }
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/AdBreak.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/AdBreak.java
new file mode 100644
index 000000000..9a950434d
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/AdBreak.java
@@ -0,0 +1,7 @@
+package com.amazon.avod.media.ads;
+
+import com.amazon.avod.media.TimeSpan;
+
+public interface AdBreak {
+ TimeSpan getDurationExcludingAux();
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakState.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakState.java
new file mode 100644
index 000000000..f417660ed
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakState.java
@@ -0,0 +1,4 @@
+package com.amazon.avod.media.ads.internal.state;
+
+public abstract class AdBreakState extends AdEnabledPlaybackState {
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakTrigger.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakTrigger.java
new file mode 100644
index 000000000..f8b399565
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakTrigger.java
@@ -0,0 +1,18 @@
+package com.amazon.avod.media.ads.internal.state;
+
+import com.amazon.avod.media.ads.AdBreak;
+import com.amazon.avod.media.TimeSpan;
+
+public class AdBreakTrigger {
+ public AdBreak getBreak() {
+ throw new UnsupportedOperationException();
+ }
+
+ public TimeSpan getSeekTarget() {
+ throw new UnsupportedOperationException();
+ }
+
+ public TimeSpan getSeekStartPosition() {
+ throw new UnsupportedOperationException();
+ }
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlaybackState.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlaybackState.java
new file mode 100644
index 000000000..445aad580
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlaybackState.java
@@ -0,0 +1,8 @@
+package com.amazon.avod.media.ads.internal.state;
+
+import com.amazon.avod.fsm.StateBase;
+import com.amazon.avod.media.playback.state.PlayerStateType;
+import com.amazon.avod.media.playback.state.trigger.PlayerTriggerType;
+
+public class AdEnabledPlaybackState extends StateBase {
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlayerTriggerType.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlayerTriggerType.java
new file mode 100644
index 000000000..e7951e934
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlayerTriggerType.java
@@ -0,0 +1,5 @@
+package com.amazon.avod.media.ads.internal.state;
+
+public enum AdEnabledPlayerTriggerType {
+ NO_MORE_ADS_SKIP_TRANSITION
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState.java
new file mode 100644
index 000000000..07c198013
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState.java
@@ -0,0 +1,4 @@
+package com.amazon.avod.media.ads.internal.state;
+
+public class ServerInsertedAdBreakState extends AdBreakState {
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/VideoPlayer.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/VideoPlayer.java
new file mode 100644
index 000000000..af3d0bee5
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/VideoPlayer.java
@@ -0,0 +1,7 @@
+package com.amazon.avod.media.playback;
+
+public interface VideoPlayer {
+ long getCurrentPosition();
+
+ void seekTo(long positionMs);
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/PlayerStateType.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/PlayerStateType.java
new file mode 100644
index 000000000..202723285
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/PlayerStateType.java
@@ -0,0 +1,4 @@
+package com.amazon.avod.media.playback.state;
+
+public interface PlayerStateType {
+}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/trigger/PlayerTriggerType.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/trigger/PlayerTriggerType.java
new file mode 100644
index 000000000..eac139f9b
--- /dev/null
+++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/trigger/PlayerTriggerType.java
@@ -0,0 +1,4 @@
+package com.amazon.avod.media.playback.state.trigger;
+
+public interface PlayerTriggerType {
+}
\ No newline at end of file
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 6bc936afb..7e9a01a9c 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -420,6 +420,14 @@ public final class app/revanced/patches/pixiv/ads/HideAdsPatchKt {
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/primevideo/ads/SkipAdsPatchKt {
+ public static final fun getSkipAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
+public final class app/revanced/patches/primevideo/misc/extension/ExtensionPatchKt {
+ public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/protonmail/signature/RemoveSentFromSignaturePatchKt {
public static final fun getRemoveSentFromSignaturePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/Fingerprints.kt
new file mode 100644
index 000000000..ac3a1c43a
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/Fingerprints.kt
@@ -0,0 +1,33 @@
+package app.revanced.patches.primevideo.ads
+
+import app.revanced.patcher.fingerprint
+import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.Opcode
+
+internal val enterServerInsertedAdBreakStateFingerprint = fingerprint {
+ accessFlags(AccessFlags.PUBLIC)
+ parameters("Lcom/amazon/avod/fsm/Trigger;")
+ returns("V")
+ opcodes(
+ Opcode.INVOKE_VIRTUAL,
+ Opcode.MOVE_RESULT_OBJECT,
+ Opcode.CONST_4,
+ Opcode.CONST_4
+ )
+ custom { method, classDef ->
+ method.name == "enter" && classDef.type == "Lcom/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState;"
+ }
+}
+
+internal val doTriggerFingerprint = fingerprint {
+ accessFlags(AccessFlags.PROTECTED)
+ returns("V")
+ opcodes(
+ Opcode.IGET_OBJECT,
+ Opcode.INVOKE_INTERFACE,
+ Opcode.RETURN_VOID
+ )
+ custom { method, classDef ->
+ method.name == "doTrigger" && classDef.type == "Lcom/amazon/avod/fsm/StateBase;"
+ }
+}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/SkipAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/SkipAdsPatch.kt
new file mode 100644
index 000000000..e43828932
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/SkipAdsPatch.kt
@@ -0,0 +1,45 @@
+package app.revanced.patches.primevideo.ads
+
+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 com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+
+@Suppress("unused")
+val skipAdsPatch = bytecodePatch(
+ name = "Skip ads",
+ description = "Automatically skips video stream ads.",
+) {
+ compatibleWith("com.amazon.avod.thirdpartyclient"("3.0.403.257"))
+
+ dependsOn(sharedExtensionPatch)
+
+ // Skip all the logic in ServerInsertedAdBreakState.enter(), which plays all the ad clips in this
+ // ad break. Instead, force the video player to seek over the entire break and reset the state machine.
+ execute {
+ // Force doTrigger() access to public so we can call it from our extension.
+ doTriggerFingerprint.method.accessFlags = AccessFlags.PUBLIC.value;
+
+ val getPlayerIndex = enterServerInsertedAdBreakStateFingerprint.patternMatch!!.startIndex
+ enterServerInsertedAdBreakStateFingerprint.method.apply {
+ // Get register that stores VideoPlayer:
+ // invoke-virtual ->getPrimaryPlayer()
+ // move-result-object { playerRegister }
+ val playerRegister = getInstruction(getPlayerIndex + 1).registerA
+
+ // Reuse the params from the original method:
+ // p0 = ServerInsertedAdBreakState
+ // p1 = AdBreakTrigger
+ addInstructions(
+ getPlayerIndex + 2,
+ """
+ invoke-static { p0, p1, v$playerRegister }, Lapp/revanced/extension/primevideo/ads/SkipAdsPatch;->enterServerInsertedAdBreakState(Lcom/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState;Lcom/amazon/avod/media/ads/internal/state/AdBreakTrigger;Lcom/amazon/avod/media/playback/VideoPlayer;)V
+ return-void
+ """
+ )
+ }
+ }
+}
+
diff --git a/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/ExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/ExtensionPatch.kt
new file mode 100644
index 000000000..34f4f9b36
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/ExtensionPatch.kt
@@ -0,0 +1,5 @@
+package app.revanced.patches.primevideo.misc.extension
+
+import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
+
+val sharedExtensionPatch = sharedExtensionPatch("primevideo", applicationInitHook)
diff --git a/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/Hooks.kt b/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/Hooks.kt
new file mode 100644
index 000000000..763c2bfd5
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/Hooks.kt
@@ -0,0 +1,9 @@
+package app.revanced.patches.primevideo.misc.extension
+
+import app.revanced.patches.shared.misc.extension.extensionHook
+
+internal val applicationInitHook = extensionHook {
+ custom { method, classDef ->
+ method.name == "onCreate" && classDef.endsWith("/SplashScreenActivity;")
+ }
+}