oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1// room-hearing.test.mjs — room.js's hearing watch (spec §15.20), gated
2// without a browser.
3//
4// node --test test/browser/room-hearing.test.mjs
5//
6// Same trick as room-mute.test.mjs: the functions are lifted out of the
7// shipped room.js by name, and so are the constants they read, so a
8// rename or a retuned threshold fails here loudly instead of passing
9// against a copy.
10//
11// audioSample one stats report read against the last: the level, and
12// whether it was sound at all
13// judgeSilence the five-second rule, and what it refuses to count
14// selfMicWatched whether our own microphone may be judged at all:
15// never alone, never muted
16// judgeSpeaking the speaking highlight's hysteresis
17// hearingShown muted, then blocked, then no sound, then live sound
18//
19// The speaking glow that replaced the meter has room-glow.test.mjs.
20// remoteNotice what a peer's own report says about hearing us
21import { test } from 'node:test';
22import assert from 'node:assert/strict';
23import { readFileSync } from 'node:fs';
24import { fileURLToPath } from 'node:url';
25
26const roomJS = fileURLToPath(new URL('../../internal/calls/static/room.js', import.meta.url));
27const src = readFileSync(roomJS, 'utf8');
28
29function lift(source, name) {
30 const start = source.indexOf('function ' + name + '(');
31 assert.notEqual(start, -1, name + ' is not in room.js any more — this test is stale, not passing');
32 let depth = 0;
33 for (let i = source.indexOf('{', start); i < source.length; i++) {
34 if (source[i] === '{') depth += 1;
35 else if (source[i] === '}') {
36 depth -= 1;
37 if (depth === 0) return source.slice(start, i + 1);
38 }
39 }
40 assert.fail('unbalanced braces reading ' + name + ' out of room.js');
41}
42
43// liftConst reads one `const NAME = <number>;` line, so the thresholds
44// under test are the shipped ones.
45function liftConst(name) {
46 const m = src.match(new RegExp('const ' + name + ' = ([0-9.]+);'));
47 assert.ok(m, name + ' is not a numeric const in room.js any more');
48 return 'const ' + name + ' = ' + m[1] + ';';
49}
50
51const CONSTS = ['FLAT_LEVEL', 'NO_SOUND_AFTER', 'SPEAK_ON', 'SPEAK_OFF', 'SPEAK_HOLD']
52 .map(liftConst).join('\n');
53const consts = new Function(CONSTS + '; return { FLAT_LEVEL, NO_SOUND_AFTER, SPEAK_HOLD };')();
54
55const build = (name) =>
56 // eslint-disable-next-line no-new-func
57 new Function(CONSTS + '\n' + lift(src, name) + '; return ' + name + ';')();
58
59const audioSample = build('audioSample');
60const judgeSilence = build('judgeSilence');
61const selfMicWatched = build('selfMicWatched');
62const judgeSpeaking = build('judgeSpeaking');
63const hearingShown = build('hearingShown');
64const remoteNotice = build('remoteNotice');
65const hearingRecord = build('hearingRecord');
66
67test('the thresholds are the ones the spec names', () => {
68 // -80 dBFS, and five seconds.
69 assert.equal(consts.FLAT_LEVEL, 0.0001);
70 assert.equal(consts.NO_SOUND_AFTER, 5000);
71});
72
73test('audioSample prefers audioLevel, and falls back to energy over time', () => {
74 const first = audioSample(null, { audioLevel: 0.2, packetsReceived: 10, totalAudioEnergy: 1, totalSamplesDuration: 2 });
75 assert.equal(first.level, 0.2);
76 assert.equal(first.heard, true);
77 assert.equal(first.grew, false, 'a first report has nothing to have grown from');
78 // No audioLevel: the RMS since the last poll stands in. Energy is the
79 // sum of squares times duration, so 0.0004 J over 1 s is RMS 0.02.
80 const prev = { energy: 1, duration: 2, packets: 10 };
81 const next = audioSample(prev, { packetsReceived: 60, totalAudioEnergy: 1.0004, totalSamplesDuration: 3 });
82 assert.ok(Math.abs(next.level - 0.02) < 1e-9, String(next.level));
83 assert.equal(next.grew, true);
84 // A beep between two polls: the instant level reads flat, the energy
85 // says there was sound.
86 const beep = audioSample(prev, { audioLevel: 0, packetsReceived: 60, totalAudioEnergy: 1.0004, totalSamplesDuration: 3 });
87 assert.equal(beep.level, 0);
88 assert.equal(beep.heard, true);
89 // Flat is flat.
90 const flat = audioSample(prev, { audioLevel: 0, packetsReceived: 60, totalAudioEnergy: 1, totalSamplesDuration: 3 });
91 assert.equal(flat.heard, false);
92 // Counters that went backwards (a replaced track, a new source) are
93 // not read as a burst of sound.
94 const reset = audioSample(prev, { packetsReceived: 5, totalAudioEnergy: 0, totalSamplesDuration: 0.2 });
95 assert.equal(reset.level, 0);
96 assert.equal(reset.heard, false);
97 assert.equal(reset.grew, false);
98 // A report with nothing in it draws as silence, but is not evidence of
99 // it: the five-second rule is only fed what a browser measured.
100 const blank = audioSample(null, { packetsReceived: 5 });
101 assert.equal(blank.level, 0);
102 assert.equal(blank.measurable, false);
103 assert.equal(first.measurable, true);
104 assert.equal(audioSample(prev, { totalAudioEnergy: 1, totalSamplesDuration: 3 }).measurable, true);
105});
106
107test('judgeSilence: five flat seconds of an arriving, eligible stream, and not a moment sooner', () => {
108 const rec = hearingRecord();
109 const t0 = 1000000;
110 assert.equal(judgeSilence(rec, t0, true, true, false), false);
111 assert.equal(judgeSilence(rec, t0 + 4999, true, true, false), false);
112 assert.equal(judgeSilence(rec, t0 + 5000, true, true, false), true);
113 assert.equal(rec.silent, true);
114 // Any sound ends it at once.
115 assert.equal(judgeSilence(rec, t0 + 5200, true, true, true), false);
116 assert.equal(rec.silent, false);
117 // And the count starts again from the next flat poll.
118 assert.equal(judgeSilence(rec, t0 + 5400, true, true, false), false);
119 assert.equal(judgeSilence(rec, t0 + 10399, true, true, false), false);
120 assert.equal(judgeSilence(rec, t0 + 10400, true, true, false), true);
121});
122
123test('our own microphone: alone never warns; with someone, five silent seconds do', () => {
124 // Mac 0.4.6, 2026-10-03: alone in a call, nothing is sent, and the
125 // warning said a working microphone was dead. Same rule in the browser.
126 const rec = hearingRecord();
127 const t0 = 1000000;
128 for (let t = 0; t <= 30000; t += 200) {
129 judgeSilence(rec, t0 + t, selfMicWatched(true, false, false), true, false);
130 assert.equal(rec.silent, false, 'alone, at ' + t + ' ms');
131 }
132 // Muted, with someone: the Muted state says it, not this (0.3.6).
133 for (let t = 30000; t <= 40000; t += 200) {
134 judgeSilence(rec, t0 + t, selfMicWatched(true, true, true), true, false);
135 assert.equal(rec.silent, false, 'muted, at ' + t + ' ms');
136 }
137 // Unmuted, someone connected, flat: the warning, five seconds on.
138 const start = t0 + 40200;
139 assert.equal(judgeSilence(rec, start, selfMicWatched(true, false, true), true, false), false);
140 assert.equal(judgeSilence(rec, start + 4999, selfMicWatched(true, false, true), true, false), false);
141 assert.equal(judgeSilence(rec, start + 5000, selfMicWatched(true, false, true), true, false), true);
142 // They leave: gone at once.
143 assert.equal(judgeSilence(rec, start + 5200, selfMicWatched(true, false, false), true, false), false);
144});
145
146test('judgeSilence counts nothing that is not the sender’s silence', () => {
147 const t0 = 1000000;
148 for (const [eligible, arriving, why] of [
149 [false, true, 'a muted peer, or a tab that is not playing, is not judged'],
150 [true, false, 'no packets is a connection’s problem, not a microphone’s'],
151 ]) {
152 const rec = hearingRecord();
153 judgeSilence(rec, t0, eligible, arriving, false);
154 assert.equal(judgeSilence(rec, t0 + 60000, eligible, arriving, false), false, why);
155 }
156 // Eligibility lost halfway resets the count rather than pausing it.
157 const rec = hearingRecord();
158 judgeSilence(rec, t0, true, true, false);
159 judgeSilence(rec, t0 + 3000, false, true, false);
160 assert.equal(judgeSilence(rec, t0 + 6000, true, true, false), false);
161 assert.equal(judgeSilence(rec, t0 + 11000, true, true, false), true);
162});
163
164test('judgeSpeaking holds through a gap between syllables', () => {
165 const rec = hearingRecord();
166 assert.equal(judgeSpeaking(rec, 0.01), false);
167 assert.equal(judgeSpeaking(rec, 0.1), true);
168 // Between the two thresholds: held, and the hold is not spent.
169 for (let i = 0; i < 10; i++) assert.equal(judgeSpeaking(rec, 0.03), true);
170 // Under the lower one: off only after the hold.
171 for (let i = 1; i < consts.SPEAK_HOLD; i++) assert.equal(judgeSpeaking(rec, 0), true, 'poll ' + i);
172 assert.equal(judgeSpeaking(rec, 0), false);
173 // And a level between the two does not turn it back on.
174 assert.equal(judgeSpeaking(rec, 0.03), false);
175});
176
177test('hearingShown: muted, then blocked, then no sound, then the meter', () => {
178 const on = { muted: false, cameraOff: false };
179 const muted = { muted: true, cameraOff: false };
180 assert.equal(hearingShown(muted, true, true), 'muted', 'a muted peer shows Muted and nothing else');
181 assert.equal(hearingShown(on, true, true), 'blocked', 'this browser refusing sound comes before their silence');
182 assert.equal(hearingShown(on, false, true), 'silent');
183 assert.equal(hearingShown(on, false, false), 'meter');
184 // Unknown is not muted (§15.19).
185 assert.equal(hearingShown(null, false, false), 'meter');
186 assert.equal(hearingShown(null, true, false), 'blocked');
187});
188
189test('remoteNotice: their browser blocking, or no sound from us while we are not muted', () => {
190 const me = 'c'.repeat(32);
191 const on = { muted: false, cameraOff: false };
192 assert.equal(remoteNotice('Tony', null, me, false), '');
193 assert.equal(remoteNotice('Tony', on, me, false), '');
194 assert.equal(remoteNotice('Tony', { ...on, audioBlocked: true }, me, false),
195 'Tony’s browser is blocking sound — they need to click to hear you.');
196 assert.equal(remoteNotice('Tony', { ...on, noSoundFrom: [me] }, me, false),
197 'Tony isn’t getting any sound from you.');
198 assert.equal(remoteNotice('Tony', { ...on, noSoundFrom: [me] }, me, true), '',
199 'muted, we are sending nothing on purpose and there is nothing to fix');
200 assert.equal(remoteNotice('Tony', { ...on, noSoundFrom: ['d'.repeat(32)] }, me, false), '',
201 'somebody else’s silence is not ours');
202});
203