Skip to content

feat(Prime Video): Add Skip ads patch #4824

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

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from

Conversation

hoo-dles
Copy link
Contributor

@hoo-dles hoo-dles commented Apr 18, 2025

This is a first pass of a patch for Amazon Prime Video to skip ads. Ad breaks seem to be baked into the incoming stream, so the best we can do is force the video player to seek over them.

I could use some help with more extensive testing since the Prime Video app has like 5 different video player implementations and I'm not sure when (if?) the ones that I didn't patch are used.

@hoo-dles
Copy link
Contributor Author

Related to feature request: #4676

@LisoUseInAIKyrios LisoUseInAIKyrios linked an issue Apr 18, 2025 that may be closed by this pull request
3 tasks
@LisoUseInAIKyrios LisoUseInAIKyrios changed the title feat(Prime Video): Initial commit of Skip Ads patch feat(Prime Video): Add Skip ads patch Apr 18, 2025
@LisoUseInAIKyrios
Copy link
Contributor

For the other player implementations, it's worth checking an Android tablet (Android Studio device emulator).

@Suppress("unused")
val skipAdsPatch = bytecodePatch(
name = "Skip ads",
description = "Automatically skip video stream ads.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description = "Automatically skip video stream ads.",
description = "Automatically skips video stream ads.",

) {
compatibleWith("com.amazon.avod.thirdpartyclient"("3.0.403.257"))

dependsOn(sharedExtensionPatch("primevideo"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the shared extension create a val sharedExtensionPatch = sharedExtensionPatch("primevideo") just like its done for the other apps for consistency.

Comment on lines +19 to +20
// We're going to skip all the logic in ServerInsertedAdBreakState.enter(), which plays all the ad clips in this
// ad break. Instead, we'll force the video player to seek over the entire break and reset the state machine.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// We're going to skip all the logic in ServerInsertedAdBreakState.enter(), which plays all the ad clips in this
// ad break. Instead, we'll force the video player to seek over the entire break and reset the state machine.
// 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.

}

android {
namespace = "app.revanced.extension"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this set already by the patches Gradle plugin?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so. This could be removed.

Comment on lines +7 to +11
compileSdk = 34

defaultConfig {
minSdk = 21
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this also set already by the patches Gradle plugin?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compile sdk is already set and can be removed here.

But minSdk for Prime video is Android 5.0 and not the default Android 6.0


@SuppressWarnings("unused")
public final class SkipAdsPatch {
public static void ServerInsertedAdBreakState_enter(ServerInsertedAdBreakState state, AdBreakTrigger trigger, VideoPlayer player) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the naming convention we use for extensions is respected here. Please match the style. Something like "enterServerInsertedAdBreakState"

Comment on lines +15 to +24
if (trigger.getSeekStartPosition() != null)
// if scrubbing over ad, seek straight to it
player.seekTo(trigger.getSeekTarget().getTotalMilliseconds());
else
// if naturally entering ad, seek to end of ad break
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));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (trigger.getSeekStartPosition() != null)
// if scrubbing over ad, seek straight to it
player.seekTo(trigger.getSeekTarget().getTotalMilliseconds());
else
// if naturally entering ad, seek to end of ad break
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));
}
if (trigger.getSeekStartPosition() != null)
// If scrubbing over an ad, seek straight to it.
player.seekTo(trigger.getSeekTarget().getTotalMilliseconds());
else
// If naturally entering an ad, seek to end the ad break.
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));
}

Comment on lines +16 to +17
// if scrubbing over ad, seek straight to it
player.seekTo(trigger.getSeekTarget().getTotalMilliseconds());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of this?

addInstructions(
getPlayerIndex + 2,
"""
invoke-static {p0, p1, v${playerRegister}}, Lapp/revanced/extension/primevideo/ads/SkipAdsPatch;->ServerInsertedAdBreakState_enter(Lcom/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState;Lcom/amazon/avod/media/ads/internal/state/AdBreakTrigger;Lcom/amazon/avod/media/playback/VideoPlayer;)V
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
invoke-static {p0, p1, v${playerRegister}}, Lapp/revanced/extension/primevideo/ads/SkipAdsPatch;->ServerInsertedAdBreakState_enter(Lcom/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState;Lcom/amazon/avod/media/ads/internal/state/AdBreakTrigger;Lcom/amazon/avod/media/playback/VideoPlayer;)V
invoke-static { p0, p1, v$playerRegister }, Lapp/revanced/extension/primevideo/ads/SkipAdsPatch;->ServerInsertedAdBreakState_enter(Lcom/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState;Lcom/amazon/avod/media/ads/internal/state/AdBreakTrigger;Lcom/amazon/avod/media/playback/VideoPlayer;)V

enterServerInsertedAdBreakStateFingerprint.method.apply {
// Get register that stores VideoPlayer:
// invoke-virtual ->getPrimaryPlayer()
// move-result-object {vx}
Copy link
Member

@oSumAtrIX oSumAtrIX Apr 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// move-result-object {vx}
// move-result-object { playerRegister }

Opcode.CONST_4
)
custom { method, classDef ->
method.name == "enter" && classDef.type == "Lcom/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState;"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is ".type" needed here? ClassDef extends CharSequence

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed since it's not using any String methods (such as endsWith).

but not sure if using == will show a false warning about comparing different types since ClassDef is a CharSequence not a String. I guess it could be with or without .type, but I find it slightly more convoluted comparing the non-String ClassDef object directly to a static String.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat: Prime Video Hide Ads
4 participants