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;") + } +}