Skip to content

Share the sound of the controlled computer - #2

Open
math65 wants to merge 5 commits into
accessolutions:mainfrom
math65:remote-audio
Open

Share the sound of the controlled computer#2
math65 wants to merge 5 commits into
accessolutions:mainfrom
math65:remote-audio

Conversation

@math65

@math65 math65 commented Aug 30, 2026

Copy link
Copy Markdown

Adds optional peer to peer sharing of the sound the controlled computer plays,
alongside the existing screen sharing or on its own. Toggled with
NVDA+Control+Shift+J.

What it does

The controlling computer hears the whole mix of the controlled one — music,
videos, alert sounds, the other side of a call — with that computer's own screen
reader left out, so what arrives is the sound of the machine rather than a
recording of NVDA talking over it. The speech it reports separately keeps being
spoken through the relay, with the synthesiser, voice and rate of whoever is
listening.

Sound can be used without any picture. When none is asked for, none is captured
or encoded, so a sound only session costs a fraction of a shared screen.

Consent and privacy

The controlled computer is always asked before anything is sent, and the answer
lasts only for the session. Adding or removing the sound changes what is being
asked for, so its user is asked again. A new setting, enabled by default,
lets a computer refuse outright: when it is cleared the request is denied
without anyone being asked, and clearing it does not affect screen sharing. Nothing
is ever recorded at either end, and the stream travels directly between the two
computers.

Leaving the screen reader out

This is done by a small helper shipped with the add-on, which uses the process
loopback capture Windows gained in Windows 10 21H2 — an API a web page cannot
reach, which is why the browser capture alone cannot do it.

On an older Windows, or if the helper cannot start, the add-on falls back to the
browser capture as before, screen reader included. The speech that computer
reports is then not spoken a second time, so it is heard as it really sounds
rather than announced twice. A second setting controls that. Braille is
unaffected throughout.

If the link drops without the session ending, the sound stops but the speech is
handed back for as long as the interruption lasts, announced by Sound
interrupted
and then Connection restored.

About the helper binary

bin/nvda_audio_capture.exe is built from native-audio-capture/, included in
this branch with its CMake build and a README covering why it exists, how to
rebuild it and how to confirm the exclusion on a single machine — following the
precedent of url_handler.cpp next to url_handler.exe. It is linked
statically, needs nothing installed alongside it and stays around 256 kB, under
the check-added-large-files ceiling.

Testing

No automated tests exist in this repository, so this was exercised by building
the add-on and installing it in NVDA: between two instances on one machine, and
between two machines through a relay. The helper's capture modes were also
checked on their own, with and without the exclusion, using the measure mode
documented in its README.

protocol.md documents the added messages and capability. readme.md documents
the feature, and the French catalogue is updated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UonSBbAsT8n34n5aH14puS

math65 and others added 5 commits August 29, 2026 12:57
The controlling computer can now hear what the controlled one plays -
music, videos, alert sounds, the other side of a call - alongside the
shared screen or on its own. NVDA+Control+Shift+J starts and stops it,
and the controlled computer is asked before anything is sent.

The sound travels as a WebRTC audio track on the peer to peer session
screen sharing already establishes, so it never goes through the relay.
A sound only session sends no picture at all.

The sound is captured with the screen reader of that computer left out,
by a small native helper using the process loopback mode Windows exposes
from build 20348. No web API reaches that mode, so the helper serves the
samples to the signalling page over a loopback WebSocket and the page
rebuilds a track from them. It repeats the protections of the existing
local bridge - loopback only, a port chosen by the system for each
session, a token compared in constant time and a strict origin check -
with the token passed on standard input rather than on the command line,
which other processes can read.

Leaving the screen reader out is what lets the forwarded speech keep
being spoken while the sound flows. It reaches the listener faster than
the audio stream and with their own synthesiser, voice and rate.

On an older Windows, or when the helper cannot start, the browser
captures the whole mix as before and the forwarded speech is silenced
instead, so that the remote screen reader is not heard twice. Which of
the two applies is answered to the peer, and a peer that predates this
says nothing, which reads as the browser capture and keeps the previous
behaviour.

A connection that drops without ending the session now hands the
forwarded speech back for as long as the interruption lasts. It used to
leave the user with neither the sound nor the speech, and no message,
for as long as it took to turn into a failure.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01AV4tMCFa9DeRWUSGbKuCfA
The repository refuses files over 500 kB, through check-added-large-files
in the pre-commit configuration, and a statically linked C++ binary sails
past that without trying: the helper weighed 749 kB.

Optimising for size rather than speed, dropping exceptions and RTTI,
giving each function its own section and letting the linker discard the
unreachable ones brings it to 256 kB. Nothing it does changes: excluding
NVDA from the capture, the loopback WebSocket and its checks, and the
offer the page then produces were all measured again against the smaller
binary.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01AV4tMCFa9DeRWUSGbKuCfA
The controlled computer only ever asked for them once, when it joined the
channel, while stop() drops the ones the finished session used. Every session
after the first therefore started with an empty list and gathered nothing but
host candidates, so it could only ever link up with a computer already on the
same network. Two computers on different connections negotiated on local
addresses alone and never linked up, with nothing said anywhere about why.

The controlling side already asked again for each session, and says why in a
comment: the relay only hands the servers to clients which asked, and the
credentials expire. The controlled side simply never followed that rule.

Ask again when a request arrives, before putting the question to the user: the
time taken to answer a dialog is far more than the relay needs to reply.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01MNU1WA7oTJLVQhrZXzsoY3
Opus is negotiated in mono unless the session description says otherwise, and a
browser never says so on its own. The sound was captured in stereo, fed to the
encoder in stereo, and given a ceiling of 128 kbit/s meant for music, only to be
mixed down to one channel before it left. The comments here and the readme both
described a stereo stream that never existed.

Add "stereo=1", which tells the other end this one can decode two channels and
is what makes it encode them, along with "sprop-stereo=1" and the ceiling, to
the Opus parameters of every description this end sends. The payload number is
read from the rtpmap line rather than assumed, existing parameters are kept, and
a description offering Opus without any parameters gets a line of its own.

Both computers run this, so both directions carry two channels. One running an
older build announces nothing and keeps the mono it understands.

Measured on the page itself: the parameters come back parsed and reordered by
the browser, and the offer now carries stereo=1 and sprop-stereo=1.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01MNU1WA7oTJLVQhrZXzsoY3
The add-on ships bin/nvda_audio_capture.exe, but not the code it is built
from, so the one binary in the package that is neither vendored nor
third-party was also the only one nobody could rebuild or read. The
repository already answers this question the other way round: url_handler.exe
sits next to url_handler.cpp.

Add the source, its CMake build and a README covering why the helper exists,
how to rebuild it, and how to confirm on a single machine that the exclusion
works. The build options were already commented; the README explains the
program itself.

Its messages were written in French while the rest of the repository is in
English. Translate them, and rebuild the binary so that what ships matches
the source that is now next to it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UonSBbAsT8n34n5aH14puS
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.

1 participant