diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch.java
new file mode 100644
index 000000000..cf010a151
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch.java
@@ -0,0 +1,30 @@
+package app.revanced.extension.youtube.patches;
+
+import app.revanced.extension.youtube.settings.Settings;
+
+@SuppressWarnings("unused")
+public class PauseOnAudioInterruptPatch {
+
+ private static final int AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK = -3;
+ private static final int AUDIOFOCUS_LOSS_TRANSIENT = -2;
+
+ /**
+ * Injection point for AudioFocusRequest builder.
+ * Returns true if audio ducking should be disabled (willPauseWhenDucked = true).
+ */
+ public static boolean shouldPauseOnAudioInterrupt() {
+ return Settings.PAUSE_ON_AUDIO_INTERRUPT.get();
+ }
+
+ /**
+ * Injection point for onAudioFocusChange callback.
+ * Converts AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK to AUDIOFOCUS_LOSS_TRANSIENT
+ * when the setting is enabled, causing YouTube to pause instead of ducking.
+ */
+ public static int overrideAudioFocusChange(int focusChange) {
+ if (Settings.PAUSE_ON_AUDIO_INTERRUPT.get() && focusChange == AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
+ return AUDIOFOCUS_LOSS_TRANSIENT;
+ }
+ return focusChange;
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
index 62fa7bcae..21f3184b5 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
@@ -356,6 +356,7 @@ public class Settings extends BaseSettings {
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1, false, false);
public static final BooleanSetting LOOP_VIDEO = new BooleanSetting("revanced_loop_video", FALSE);
public static final BooleanSetting LOOP_VIDEO_BUTTON = new BooleanSetting("revanced_loop_video_button", FALSE);
+ public static final BooleanSetting PAUSE_ON_AUDIO_INTERRUPT = new BooleanSetting("revanced_pause_on_audio_interrupt", FALSE, true);
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_CHAPTERS = new BooleanSetting("revanced_disable_haptic_feedback_chapters", FALSE);
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING = new BooleanSetting("revanced_disable_haptic_feedback_precise_seeking", FALSE);
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 86d8f980b..8142a54dc 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -1696,6 +1696,10 @@ public final class app/revanced/patches/youtube/misc/announcements/Announcements
public static final fun getAnnouncementsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatchKt {
+ public static final fun getPauseOnAudioInterruptPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/youtube/misc/autorepeat/AutoRepeatPatchKt {
public static final fun getAutoRepeatPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/Fingerprints.kt
new file mode 100644
index 000000000..5fb44f20f
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/Fingerprints.kt
@@ -0,0 +1,14 @@
+package app.revanced.patches.youtube.misc.audiofocus
+
+import app.revanced.patcher.fingerprint
+
+internal val audioFocusChangeListenerFingerprint = fingerprint {
+ strings(
+ "AudioFocus DUCK",
+ "AudioFocus loss; Will lower volume",
+ )
+}
+
+internal val audioFocusRequestBuilderFingerprint = fingerprint {
+ strings("Can't build an AudioFocusRequestCompat instance without a listener")
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt
new file mode 100644
index 000000000..2f6317d6e
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt
@@ -0,0 +1,66 @@
+package app.revanced.patches.youtube.misc.audiofocus
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.util.smali.ExternalLabel
+import app.revanced.patches.all.misc.resources.addResources
+import app.revanced.patches.all.misc.resources.addResourcesPatch
+import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
+import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
+import app.revanced.patches.youtube.misc.settings.PreferenceScreen
+import app.revanced.patches.youtube.misc.settings.settingsPatch
+
+private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch;"
+
+val pauseOnAudioInterruptPatch = bytecodePatch(
+ name = "Pause on audio interrupt",
+ description = "Adds an option to pause playback instead of lowering volume when other audio plays.",
+) {
+ dependsOn(
+ sharedExtensionPatch,
+ settingsPatch,
+ addResourcesPatch,
+ )
+
+ compatibleWith(
+ "com.google.android.youtube"(
+ "20.14.43",
+ )
+ )
+
+ execute {
+ addResources("youtube", "misc.audiofocus.pauseOnAudioInterruptPatch")
+
+ PreferenceScreen.MISC.addPreferences(
+ SwitchPreference("revanced_pause_on_audio_interrupt"),
+ )
+
+ // Hook the builder method that creates AudioFocusRequest.
+ // At the start, set the willPauseWhenDucked field (b) to true if setting is enabled.
+ val builderMethod = audioFocusRequestBuilderFingerprint.method
+ val builderClass = builderMethod.definingClass
+
+ builderMethod.addInstructionsWithLabels(
+ 0,
+ """
+ invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->shouldPauseOnAudioInterrupt()Z
+ move-result v0
+ if-eqz v0, :skip_override
+ const/4 v0, 0x1
+ iput-boolean v0, p0, $builderClass->b:Z
+ """,
+ ExternalLabel("skip_override", builderMethod.getInstruction(0)),
+ )
+
+ // Also hook the audio focus change listener as a backup.
+ audioFocusChangeListenerFingerprint.method.addInstructions(
+ 0,
+ """
+ invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->overrideAudioFocusChange(I)I
+ move-result p1
+ """
+ )
+ }
+}
diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml
index 53e99d70d..9c2a55e0e 100644
--- a/patches/src/main/resources/addresources/values/strings.xml
+++ b/patches/src/main/resources/addresources/values/strings.xml
@@ -1558,6 +1558,11 @@ Tap here to learn more about DeArrow"
Loop video is on
Loop video is off
+
+ Pause on audio interrupt
+ Playback pauses when other audio plays (e.g. navigation)
+ Volume lowers when other audio plays
+
Spoof device dimensions
"Device dimensions spoofed