| index 9e3a680..224da8a 100644 |
| --- a/docs/superpowers/specs/2026-09-07-meet-design.md |
| +++ b/docs/superpowers/specs/2026-09-07-meet-design.md |
| @@ -567,7 +567,7 @@ a multi-host fleet; that is noted, not solved. |
| | `reset {to}` | an admitted peer | both peers of the pair | Under the hub lock, increment the pair's generation and send `pair {peer, gen}` to both, into their replay rings. Idempotent in effect: two resets yield one higher generation both sides converge on. | |
| | `chat {text}` | an admitted peer | all admitted peers | Fan out with the sender's name. 2 KB. Not stored. | |
| | `share {on}` | an admitted peer | all admitted peers | Fan out so tiles can be labelled. | |
| -| `media {muted, cameraOff}` (2026-09-23, §15.19) | an admitted peer | all other admitted peers | Record on the participation, carry in every later `roster` and `peer`; fan out only a change. | |
| +| `media {muted, cameraOff, audioBlocked?, noSoundFrom?}` (2026-09-23, §15.19; 2026-09-25, §15.20) | an admitted peer | all other admitted peers | Record on the participation, carry in every later `roster` and `peer`; fan out only a change. | |
| | `hello`, `roster`, `peer`, `gone`, `knock`, `knock-gone`, `full`, `ended`, `lost`, `pair`, `gap`, `state` | server | as named | `roster`, `peer`, `pair` and `gap` go only to admitted peers; a waiting guest receives `hello`, `state`, then `roster` on admission, or `lost`, or `ended`, and nothing else. | |
| |
| `offer`, `answer` and `candidate` also carry `seq` and `gen` (§4.6); |
| @@ -2295,3 +2295,71 @@ for the feature to exist, but no order of deployment breaks a call. |
| **Not logged, not stored.** Like every accepted message, a report logs |
| nothing; a refused one logs type, room, peer and outcome only (§4.4). |
| The state lives on the in-memory participation and dies with it (§2.2). |
| + |
| +### 15.20 §15.19, §5 — what the receiver can hear (2026-09-25) |
| + |
| +**Why.** On 2026-09-25 Tony, in a browser, could not hear Paul, who was |
| +on the Mac app with a multi-channel audio interface. Paul's device panel |
| +looked fine because nothing on it measured sound; Tony's side had no way |
| +to tell Paul anything; and the server keeps no call logs (§0.1), so there |
| +was nothing to read afterwards. The failure was undiagnosable from either |
| +end. Two measured facts shaped the answer: |
| + |
| +- Chromium (Chrome and Edge) does not apply its autoplay policy to a |
| + media element whose source is a WebRTC `MediaStream`: with the desktop |
| + policy forced on and no user gesture at all, the remote tile plays, |
| + while the same browser refuses a plain audio file. A refusal is still |
| + possible in Safari, Firefox and a browser set to block sound, so the |
| + recovery below is kept, but it was not what happened here. |
| +- LiveKit's AVAudioEngine audio device module connects the microphone in |
| + mono, and the conversion from a multi-channel interface keeps its first |
| + input only. A microphone on any other input sends silence. |
| + |
| +**Decision.** §15.19's `media` report gains two optional members, each |
| +carrying only bad news and omitted when there is none: |
| + |
| +- `audioBlocked: true` — this browser was refused playback of remote |
| + sound and is waiting for a click. |
| +- `noSoundFrom: [peer, …]` — the peers whose audio is arriving here |
| + (connected, packets flowing) as silence, for about five seconds, while |
| + they report themselves unmuted. At most fifteen distinct peer ids. |
| + |
| +Validation is §15.19's, extended: `muted` and `cameraOff` are required; |
| +the two new members are optional, byte-exact, at most once each, |
| +`audioBlocked` a boolean and `noSoundFrom` an array of well-formed peer |
| +ids with no repeats; no other member; the raw payload capped at 1 KB. |
| +Ids are checked for shape, not presence — the recipients ignore an id |
| +they hold no tile for. The hub stores, carries in rosters and fans out |
| +the report exactly as before, and a change to either member is a change. |
| +The payload out is re-encoded from the validated values, in the order |
| +`muted`, `cameraOff`, `audioBlocked`, `noSoundFrom`. |
| + |
| +**Compatibility.** Because the members are omitted when there is no bad |
| +news, the ordinary report is still the two-key object, and a server |
| +from before this section accepts it; only a report carrying bad news is |
| +refused there (400), and the web client then falls back to the two-key |
| +form for the rest of the page. Old clients ignore the extra members. |
| +Deploy the server before, or with, the clients. |
| + |
| +**What clients show.** Every client measures sound where it can, so a |
| +person can see it rather than infer it: |
| + |
| +- A microphone meter on one's own tile, measured on what is sent, and |
| + "Your microphone isn't picking anything up" when it has been flat for |
| + about five seconds while unmuted. |
| +- A level meter on every other person's tile, measured on what arrives, |
| + with "No sound from <name>" when their audio has been flat for about |
| + five seconds while they report themselves unmuted, and the Muted badge |
| + instead of either when they are muted. |
| +- On a person's tile, their report about us: "<name>'s browser is |
| + blocking sound — they need to click to hear you", or "<name> isn't |
| + getting any sound from you". |
| +- In a browser whose playback was refused, a page-level "Click anywhere |
| + to turn on sound"; any click or key anywhere resumes every tile. |
| +- An audio test, before a call and during one, that meters the |
| + microphone, plays a test sound, and records a few seconds through the |
| + real capture path and plays it back. The recording never leaves the |
| + device and is discarded after playback. |
| + |
| +**Not logged, not stored.** As §15.19: the report is the sender's claim, |
| +logged only on refusal, and held on the in-memory participation. |