0.12.0 — the answer can travel as sound
The headline: a handshake no longer needs two cameras. QR is asymmetric in
practice — a laptop displays a code well and reads one back badly or not at all —
so the answer direction can now be played as audible tones from one device's
speaker into another's microphone. The offer stays a QR code.
Nothing in 0.11.0 contained any of this: the version was cut two commits before
the acoustic work landed, so everything below is new to anyone installing from
npm.
Added
<qr-listen> |
the microphone half of <qr-scanner> — owns the device, the decoding and the reassembly, releases the track on every way out, and takes the same validate seam |
encodeToAudio, createAudioReceiver, frameForAudio, parseAudioFrame, loadAudioCodec |
the carrier itself, over ggwave (MIT) as an optional peer dependency — a consumer who never opens the channel installs nothing and pays nothing |
AUDIO_SAMPLE_RATE and friends |
the rate to ask an AudioContext for, plus AUDIO_PROTOCOLS, AUDIO_TRANSMISSION_LIMIT, AUDIO_CHUNK_LIMIT, AUDIO_HEADER_LENGTH, AUDIO_DEFAULT_PROTOCOL |
QR_LISTEN_STRINGS, QR_LISTEN_STRINGS_DE |
German ships with it, like every other element |
probeBrowser, probeCamera |
the two verdicts were reachable only by reading an attribute off an element, which is not an API |
The codec is handed to the element, not imported by it — listen.createReceiver = createAudioReceiver. Importing it inside <qr-listen> put ggwave into the elements bundle for every consumer, including those who never open a microphone, and its WebAssembly glue names Node's path and fs, which no browser bundle resolves. One line at the call site keeps the optional dependency optional.
Fixed
The codec is silent at 44.1 kHz. Measured across the rates browsers actually
start at: 16000, 24000, 32000, 48000 and 96000 carry a payload and back; 8000,
11025, 22050, 44100 and 88200 encode a waveform that decodes to nothing, with
no error in either direction. A browser's default rate follows the output device,
and 44.1 kHz is what a great many report — so both ends now ask for a rate the
codec survives, and both functions refuse one they cannot carry rather than
producing silence. Found by CI; it would otherwise have reached users as "the
sound thing does not work", with no log line anywhere.
The scanner used to vanish instead of explaining. Any camera failure closed
the dialog, taking away the one surface that could say why — and the host's own
message lands elsewhere on the page than the button that was just pressed. From a
person's seat that reads as nothing happened. It stays open now and names the
cause: a refused camera, no device, or an insecure origin, which used to
blame the browser. mediaDevices is simply absent over plain http, which is how
anybody testing two devices on a LAN meets it, and "this browser cannot open a
camera" sent them to change browsers over a page that works at the same address
under https.
The scanner did not fit a phone. A shadow root does not inherit the page's
reset, so below 560px the dialog asked for width: 100vw and then added 20px of
padding on each side: 430px on a 390px viewport, with the close button at the
right end of the overflow. <qr-listen> had the same two lines. Both now carry
their own box-sizing reset.
And the scanner says which camera it got. facingMode: environment names a
side of the phone, not a lens; a phone with three rear cameras picks one, and an
ultra-wide cannot focus at reading distance. The element dispatches the track's
label and resolution so a host can tell "wrong lens" from "keep aiming".
From a review of the new code:
- framing counted UTF-16 code units against a byte limit, so any non-ASCII payload was silently truncated on the wire
- two payloads could splice into one: the receiver separated transmissions by chunk count, and two answers are very often the same length. The frame header now carries a payload id
<qr-listen>could dispatchpayloadtwice across an asyncvalidate, and blanked its status line instead of showing a rejection reasonextractCompactproduced an invite that encodes, signs and verifies while describing no transport address at all when every candidate failed to pack- the keep-alive reported "could not start" when it had been stopped on purpose mid-start
Changed
The acoustic frame header gained a payload id, which changes the format. There
is no compatibility break — but only because nothing had shipped it: 0.11.0
carries neither audio.js nor qr-listen.js. After this release the same
correction would have cost a version negotiation conducted in tones across a
room.
What the tests do not reach
Both the loopback and the browser tests skip the air: one feeds the codec's
waveform back into its own receiver, the other plays it through a MediaStream
inside one page. Neither involves a speaker, a microphone or a room, so the
distances and protocols that work in practice are hand measurement rather than
CI. See #110.