Skip to content

Commit bb672c4

Browse files
authored
feat(Prime Video): Add Skip ads patch (#4824)
1 parent 0cf7a4c commit bb672c4

File tree

23 files changed

+244
-0
lines changed

23 files changed

+244
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
dependencies {
2+
compileOnly(project(":extensions:shared:library"))
3+
compileOnly(project(":extensions:primevideo:stub"))
4+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<manifest/>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package app.revanced.extension.primevideo.ads;
2+
3+
import com.amazon.avod.fsm.SimpleTrigger;
4+
import com.amazon.avod.media.ads.AdBreak;
5+
import com.amazon.avod.media.ads.internal.state.AdBreakTrigger;
6+
import com.amazon.avod.media.ads.internal.state.AdEnabledPlayerTriggerType;
7+
import com.amazon.avod.media.playback.VideoPlayer;
8+
import com.amazon.avod.media.ads.internal.state.ServerInsertedAdBreakState;
9+
10+
import app.revanced.extension.shared.Logger;
11+
12+
@SuppressWarnings("unused")
13+
public final class SkipAdsPatch {
14+
public static void enterServerInsertedAdBreakState(ServerInsertedAdBreakState state, AdBreakTrigger trigger, VideoPlayer player) {
15+
try {
16+
AdBreak adBreak = trigger.getBreak();
17+
18+
// There are two scenarios when entering the original method:
19+
// 1. Player naturally entered an ad break while watching a video.
20+
// 2. User is skipped/scrubbed to a position on the timeline. If seek position is past an ad break,
21+
// user is forced to watch an ad before continuing.
22+
//
23+
// Scenario 2 is indicated by trigger.getSeekStartPosition() != null, so skip directly to the scrubbing
24+
// target. Otherwise, just calculate when the ad break should end and skip to there.
25+
if (trigger.getSeekStartPosition() != null)
26+
player.seekTo(trigger.getSeekTarget().getTotalMilliseconds());
27+
else
28+
player.seekTo(player.getCurrentPosition() + adBreak.getDurationExcludingAux().getTotalMilliseconds());
29+
30+
// Send "end of ads" trigger to state machine so everything doesn't get whacky.
31+
state.doTrigger(new SimpleTrigger(AdEnabledPlayerTriggerType.NO_MORE_ADS_SKIP_TRANSITION));
32+
} catch (Exception ex) {
33+
Logger.printException(() -> "Failed skipping ads", ex);
34+
}
35+
}
36+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
plugins {
2+
id(libs.plugins.android.library.get().pluginId)
3+
}
4+
5+
android {
6+
namespace = "app.revanced.extension"
7+
compileSdk = 34
8+
9+
defaultConfig {
10+
minSdk = 21
11+
}
12+
13+
compileOptions {
14+
sourceCompatibility = JavaVersion.VERSION_11
15+
targetCompatibility = JavaVersion.VERSION_11
16+
}
17+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<manifest/>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.amazon.avod.fsm;
2+
3+
public final class SimpleTrigger<T> implements Trigger<T> {
4+
public SimpleTrigger(T triggerType) {
5+
}
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.amazon.avod.fsm;
2+
3+
public abstract class StateBase<S, T> {
4+
// This method orginally has protected access (modified in patch code).
5+
public void doTrigger(Trigger<T> trigger) {
6+
}
7+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.amazon.avod.fsm;
2+
3+
public interface Trigger<T> {
4+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.amazon.avod.media;
2+
3+
public final class TimeSpan {
4+
public long getTotalMilliseconds() {
5+
throw new UnsupportedOperationException();
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.amazon.avod.media.ads;
2+
3+
import com.amazon.avod.media.TimeSpan;
4+
5+
public interface AdBreak {
6+
TimeSpan getDurationExcludingAux();
7+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.amazon.avod.media.ads.internal.state;
2+
3+
public abstract class AdBreakState extends AdEnabledPlaybackState {
4+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.amazon.avod.media.ads.internal.state;
2+
3+
import com.amazon.avod.media.ads.AdBreak;
4+
import com.amazon.avod.media.TimeSpan;
5+
6+
public class AdBreakTrigger {
7+
public AdBreak getBreak() {
8+
throw new UnsupportedOperationException();
9+
}
10+
11+
public TimeSpan getSeekTarget() {
12+
throw new UnsupportedOperationException();
13+
}
14+
15+
public TimeSpan getSeekStartPosition() {
16+
throw new UnsupportedOperationException();
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.amazon.avod.media.ads.internal.state;
2+
3+
import com.amazon.avod.fsm.StateBase;
4+
import com.amazon.avod.media.playback.state.PlayerStateType;
5+
import com.amazon.avod.media.playback.state.trigger.PlayerTriggerType;
6+
7+
public class AdEnabledPlaybackState extends StateBase<PlayerStateType, PlayerTriggerType> {
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.amazon.avod.media.ads.internal.state;
2+
3+
public enum AdEnabledPlayerTriggerType {
4+
NO_MORE_ADS_SKIP_TRANSITION
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.amazon.avod.media.ads.internal.state;
2+
3+
public class ServerInsertedAdBreakState extends AdBreakState {
4+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.amazon.avod.media.playback;
2+
3+
public interface VideoPlayer {
4+
long getCurrentPosition();
5+
6+
void seekTo(long positionMs);
7+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.amazon.avod.media.playback.state;
2+
3+
public interface PlayerStateType {
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.amazon.avod.media.playback.state.trigger;
2+
3+
public interface PlayerTriggerType {
4+
}

patches/api/patches.api

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,14 @@ public final class app/revanced/patches/pixiv/ads/HideAdsPatchKt {
420420
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
421421
}
422422

423+
public final class app/revanced/patches/primevideo/ads/SkipAdsPatchKt {
424+
public static final fun getSkipAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
425+
}
426+
427+
public final class app/revanced/patches/primevideo/misc/extension/ExtensionPatchKt {
428+
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
429+
}
430+
423431
public final class app/revanced/patches/protonmail/signature/RemoveSentFromSignaturePatchKt {
424432
public static final fun getRemoveSentFromSignaturePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
425433
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package app.revanced.patches.primevideo.ads
2+
3+
import app.revanced.patcher.fingerprint
4+
import com.android.tools.smali.dexlib2.AccessFlags
5+
import com.android.tools.smali.dexlib2.Opcode
6+
7+
internal val enterServerInsertedAdBreakStateFingerprint = fingerprint {
8+
accessFlags(AccessFlags.PUBLIC)
9+
parameters("Lcom/amazon/avod/fsm/Trigger;")
10+
returns("V")
11+
opcodes(
12+
Opcode.INVOKE_VIRTUAL,
13+
Opcode.MOVE_RESULT_OBJECT,
14+
Opcode.CONST_4,
15+
Opcode.CONST_4
16+
)
17+
custom { method, classDef ->
18+
method.name == "enter" && classDef.type == "Lcom/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState;"
19+
}
20+
}
21+
22+
internal val doTriggerFingerprint = fingerprint {
23+
accessFlags(AccessFlags.PROTECTED)
24+
returns("V")
25+
opcodes(
26+
Opcode.IGET_OBJECT,
27+
Opcode.INVOKE_INTERFACE,
28+
Opcode.RETURN_VOID
29+
)
30+
custom { method, classDef ->
31+
method.name == "doTrigger" && classDef.type == "Lcom/amazon/avod/fsm/StateBase;"
32+
}
33+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package app.revanced.patches.primevideo.ads
2+
3+
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
4+
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
5+
import app.revanced.patcher.patch.bytecodePatch
6+
import app.revanced.patches.primevideo.misc.extension.sharedExtensionPatch
7+
import com.android.tools.smali.dexlib2.AccessFlags
8+
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
9+
10+
@Suppress("unused")
11+
val skipAdsPatch = bytecodePatch(
12+
name = "Skip ads",
13+
description = "Automatically skips video stream ads.",
14+
) {
15+
compatibleWith("com.amazon.avod.thirdpartyclient"("3.0.403.257"))
16+
17+
dependsOn(sharedExtensionPatch)
18+
19+
// Skip all the logic in ServerInsertedAdBreakState.enter(), which plays all the ad clips in this
20+
// ad break. Instead, force the video player to seek over the entire break and reset the state machine.
21+
execute {
22+
// Force doTrigger() access to public so we can call it from our extension.
23+
doTriggerFingerprint.method.accessFlags = AccessFlags.PUBLIC.value;
24+
25+
val getPlayerIndex = enterServerInsertedAdBreakStateFingerprint.patternMatch!!.startIndex
26+
enterServerInsertedAdBreakStateFingerprint.method.apply {
27+
// Get register that stores VideoPlayer:
28+
// invoke-virtual ->getPrimaryPlayer()
29+
// move-result-object { playerRegister }
30+
val playerRegister = getInstruction<OneRegisterInstruction>(getPlayerIndex + 1).registerA
31+
32+
// Reuse the params from the original method:
33+
// p0 = ServerInsertedAdBreakState
34+
// p1 = AdBreakTrigger
35+
addInstructions(
36+
getPlayerIndex + 2,
37+
"""
38+
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
39+
return-void
40+
"""
41+
)
42+
}
43+
}
44+
}
45+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package app.revanced.patches.primevideo.misc.extension
2+
3+
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
4+
5+
val sharedExtensionPatch = sharedExtensionPatch("primevideo", applicationInitHook)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package app.revanced.patches.primevideo.misc.extension
2+
3+
import app.revanced.patches.shared.misc.extension.extensionHook
4+
5+
internal val applicationInitHook = extensionHook {
6+
custom { method, classDef ->
7+
method.name == "onCreate" && classDef.endsWith("/SplashScreenActivity;")
8+
}
9+
}

0 commit comments

Comments
 (0)