I happened to connect my Airpods to my computer today. It never occurred to me before that the screen reader wouldn't honour the play/pause feature. Like, there's literally a say all command within #NVDASR designed to start reading the screen without further interaction, and there's this globally-recognised multimedia shortcut in windows - part of myriad keyboards, headsets (even my wired 3.5mm earbuds send it) - that the screen reader just totally ignores.
@cachondo I can see the logic; I'm not sure if anyone has ever requested it. One thing to consider would be if a play pause button on your earphones or multimedia keyboard did say all / pause for NVDA, what would happen if other media was playing? You could say if you are on a YouTube window then let it control that, but what if you've got audio playing in the background while you work? Then what does it control? Just questions to answer. Please do create an issue hough: https://github.com/nvaccess/nvda/issues
nvaccess/nvda

NVDA, the free and open source Screen Reader for Microsoft Windows - nvaccess/nvda

GitHub
@NVAccess @cachondo If NVDA controls it, then which headphones get supported?
@ppatel @NVAccess I don't want to blow this out of proportion. Just letting NVDA accept the standard play/pause as a synonym for the shift key during say all would be a good start. It's not the screenreaders or any 1 media players job to worry about other apps. If the app you're in supports the keys and you've told it to use them, it uses them. Should be that straightforward.
@cachondo @ppatel @NVAccess So long as there is the option to turn it off. I agree with the issue of which thing it would control if there was medeaa in the background.
@cachondo @ppatel Oh I'm sure there would be people interested if the feature is possible. One complication might be that while the result of pressing the button on your earpods is the same as pressing the play pause button on a multimedia keyboard, how they do it isn't the same. I'm not sure if there is a standard way they work so making it work for your eapods would also work for my soundcore earbuds & can we even tap into that in the gestures dialog? But we can investigate with an issue :)
@NVAccess @ppatel Well I can absolutely map the play/pause gesture within NVDA on the airpods, but the problem is mapping it to say all is great until you want to pause.
@cachondo @ppatel Interesting, maybe the airpods use the same way of doing it as the key on my the multimedia keyboard - but yes, we don't have a single play / pause command - we have say all, and THEN shift acts as play / pause if you will. I guess you could put in another issue requesting an option with the ability to extend shift / pause to also start say all if it's not already talking or paused?

@NVAccess @ppatel I can't get at the shift at a low enough level to capture what I want, though.
Tapping shift when speech is going pauses and resumes. That's *exactly* what i want my airpod to do. Sure, having a fancy say all just for the headset would be nifty, but just being able to pause and resume would be a major step up from the nothing we have now.

That shift must be intercepted at a fairly low level, though. even an unmapped headset key interrupts say all, I presume because NVDA treats it as a keypress.

@NVAccess @ppatel I tend to ping @tspivey when I have esoteric and weird questions in this space.

@cachondo @NVAccess @ppatel Put this in scratchpad\globalPlugins. If it breaks, you get to keep both pieces. I've tested it with the play key on my keyboard and NVDA 2026.1 beta 8. It should work going back to 2025.
from globalPluginHandler import GlobalPlugin
import inputCore
from scriptHandler import script
import queueHandler
import speech
from speech import sayAll
import tones

VK_MEDIA_PLAY_PAUSE = 0xB3

class GlobalPlugin(GlobalPlugin):
def __init__(self):
super().__init__()
self.patched = False

# We're dealing with low-level keyboard processing, so patch on the first press of play/pause.
# In case this breaks, I still need a keyboard.
@script(gesture="kb:mediaPlayPause", description="Patch")
def script_patch(self, gesture):
if not self.patched:
self.patch()
tones.beep(2000, 50)

def patch(self):
inputCore.decide_handleRawKey.register(self.handle)
self.patched = True

def terminate(self):
if self.patched:
inputCore.decide_handleRawKey.unregister(self.handle)
self.patched = False

def handle(self, vkCode=None, pressed=None, **kwargs):
if vkCode != VK_MEDIA_PLAY_PAUSE or not pressed:
return True
if not sayAll.SayAllHandler.isRunning():
sayAll.SayAllHandler.readText(sayAll.CURSOR.CARET, startedFromScript=True)
return False
if speech.getState().isPaused:
queueHandler.queueFunction(queueHandler.eventQueue, speech.pauseSpeech, False)
return False
else:
queueHandler.queueFunction(queueHandler.eventQueue, speech.pauseSpeech, True)
return False