@usero/sdk 1.1.9 → 1.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -98,6 +98,10 @@ interface RecorderStore {
98
98
  muted: boolean;
99
99
  mutedSinceMs: number | null;
100
100
  mutedSegments: MutedSegment[];
101
+ micSilent: boolean;
102
+ silenceMonitor: {
103
+ stop(): void;
104
+ } | null;
101
105
  muteToastShown: boolean;
102
106
  muteToastTimers: number[];
103
107
  notes: InFlightNote[];
@@ -110,13 +114,19 @@ interface RecorderStore {
110
114
  declare function getTestSlug(queryParam: string): string | null;
111
115
  declare function isMediaRecorderSupported(): boolean;
112
116
  declare function pickMimeType(): string | undefined;
113
- declare function micChipState(store: RecorderStore): 'recording' | 'muted' | 'none' | 'connecting' | 'inactive';
117
+ declare function micChipState(store: RecorderStore): 'recording' | 'muted' | 'none' | 'connecting' | 'silent' | 'inactive';
118
+ declare function rmsDbFromSamples(samples: Float32Array | number[]): number;
119
+ declare function isStreamSilent(input: number | Float32Array | number[]): boolean;
114
120
  declare function userTest(options?: UserTestOptions): UseroPlugin;
115
121
  declare const __test__: {
116
122
  getTestSlug: typeof getTestSlug;
117
123
  pickMimeType: typeof pickMimeType;
118
124
  isMediaRecorderSupported: typeof isMediaRecorderSupported;
119
125
  micChipState: typeof micChipState;
126
+ isStreamSilent: typeof isStreamSilent;
127
+ rmsDbFromSamples: typeof rmsDbFromSamples;
128
+ SILENCE_RMS_DB_THRESHOLD: number;
129
+ SILENCE_FLOOR_DB: number;
120
130
  };
121
131
 
122
- export { type UserTestOptions, __test__, userTest };
132
+ export { type UserTestOptions, __test__, isStreamSilent, rmsDbFromSamples, userTest };
@@ -98,6 +98,10 @@ interface RecorderStore {
98
98
  muted: boolean;
99
99
  mutedSinceMs: number | null;
100
100
  mutedSegments: MutedSegment[];
101
+ micSilent: boolean;
102
+ silenceMonitor: {
103
+ stop(): void;
104
+ } | null;
101
105
  muteToastShown: boolean;
102
106
  muteToastTimers: number[];
103
107
  notes: InFlightNote[];
@@ -110,13 +114,19 @@ interface RecorderStore {
110
114
  declare function getTestSlug(queryParam: string): string | null;
111
115
  declare function isMediaRecorderSupported(): boolean;
112
116
  declare function pickMimeType(): string | undefined;
113
- declare function micChipState(store: RecorderStore): 'recording' | 'muted' | 'none' | 'connecting' | 'inactive';
117
+ declare function micChipState(store: RecorderStore): 'recording' | 'muted' | 'none' | 'connecting' | 'silent' | 'inactive';
118
+ declare function rmsDbFromSamples(samples: Float32Array | number[]): number;
119
+ declare function isStreamSilent(input: number | Float32Array | number[]): boolean;
114
120
  declare function userTest(options?: UserTestOptions): UseroPlugin;
115
121
  declare const __test__: {
116
122
  getTestSlug: typeof getTestSlug;
117
123
  pickMimeType: typeof pickMimeType;
118
124
  isMediaRecorderSupported: typeof isMediaRecorderSupported;
119
125
  micChipState: typeof micChipState;
126
+ isStreamSilent: typeof isStreamSilent;
127
+ rmsDbFromSamples: typeof rmsDbFromSamples;
128
+ SILENCE_RMS_DB_THRESHOLD: number;
129
+ SILENCE_FLOOR_DB: number;
120
130
  };
121
131
 
122
- export { type UserTestOptions, __test__, userTest };
132
+ export { type UserTestOptions, __test__, isStreamSilent, rmsDbFromSamples, userTest };
@@ -725,7 +725,9 @@ function micChipState(store) {
725
725
  if (store.micAcquiring) return "connecting";
726
726
  return "none";
727
727
  }
728
- return store.muted ? "muted" : "recording";
728
+ if (store.muted) return "muted";
729
+ if (store.micSilent) return "silent";
730
+ return "recording";
729
731
  }
730
732
  function renderIndicatorState(store) {
731
733
  const root = store.indicatorRoot;
@@ -738,7 +740,8 @@ function renderIndicatorState(store) {
738
740
  if (!(dot instanceof HTMLElement) || !mic || !(micIcon instanceof HTMLElement) || !(micLabel instanceof HTMLElement) || !btn) return;
739
741
  dot.setAttribute("data-state", store.indicatorState);
740
742
  const chipState = micChipState(store);
741
- mic.setAttribute("data-mic-state", chipState === "inactive" ? "none" : chipState);
743
+ const micStateAttr = chipState === "inactive" || chipState === "silent" ? "none" : chipState;
744
+ mic.setAttribute("data-mic-state", micStateAttr);
742
745
  mic.removeAttribute("data-mic-fail");
743
746
  if (chipState === "none") mic.setAttribute("data-mic-fail", store.micFailReason ?? "blocked");
744
747
  switch (store.indicatorState) {
@@ -782,6 +785,13 @@ function renderIndicatorState(store) {
782
785
  mic.setAttribute("aria-pressed", "false");
783
786
  mic.setAttribute("tabindex", "-1");
784
787
  break;
788
+ case "silent":
789
+ micIcon.innerHTML = MIC_MUTED_ICON_SVG;
790
+ micLabel.textContent = "We can't hear you, tap to recheck";
791
+ mic.setAttribute("aria-label", "We can't hear your microphone. Check your input device, then tap to recheck. Recording continues.");
792
+ mic.setAttribute("aria-pressed", "false");
793
+ mic.removeAttribute("tabindex");
794
+ break;
785
795
  case "none": {
786
796
  micIcon.innerHTML = MIC_MUTED_ICON_SVG;
787
797
  const failLabel = store.micFailReason === "not-found" ? "No mic found, tap to retry" : "Mic blocked, tap to retry";
@@ -1369,9 +1379,92 @@ function enqueueChunk(store, ctx, blob) {
1369
1379
  store.pendingUploads -= 1;
1370
1380
  });
1371
1381
  }
1382
+ var SILENCE_RMS_DB_THRESHOLD = -60;
1383
+ var SILENCE_FLOOR_DB = -100;
1384
+ var SILENCE_SUSTAINED_MS = 1800;
1385
+ var SILENCE_POLL_MS = 250;
1386
+ function rmsDbFromSamples(samples) {
1387
+ const n = samples.length;
1388
+ if (n === 0) return SILENCE_FLOOR_DB;
1389
+ let sumSquares = 0;
1390
+ for (let i = 0; i < n; i += 1) {
1391
+ const s = samples[i] ?? 0;
1392
+ sumSquares += s * s;
1393
+ }
1394
+ const rms = Math.sqrt(sumSquares / n);
1395
+ if (rms <= 0) return SILENCE_FLOOR_DB;
1396
+ const db = 20 * Math.log10(rms);
1397
+ return db < SILENCE_FLOOR_DB ? SILENCE_FLOOR_DB : db;
1398
+ }
1399
+ function isStreamSilent(input) {
1400
+ const rmsDb = typeof input === "number" ? input : rmsDbFromSamples(input);
1401
+ return rmsDb <= SILENCE_RMS_DB_THRESHOLD;
1402
+ }
1403
+ function startSilenceMonitor(stream, onChange, logger) {
1404
+ const Ctor = typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : void 0;
1405
+ if (!Ctor) return null;
1406
+ let audioCtx;
1407
+ let source;
1408
+ let analyser;
1409
+ try {
1410
+ audioCtx = new Ctor();
1411
+ source = audioCtx.createMediaStreamSource(stream);
1412
+ analyser = audioCtx.createAnalyser();
1413
+ analyser.fftSize = 2048;
1414
+ source.connect(analyser);
1415
+ } catch (err) {
1416
+ logger.warn("silence monitor: failed to attach analyser", err);
1417
+ return null;
1418
+ }
1419
+ const buffer = new Float32Array(analyser.fftSize);
1420
+ let reportedSilent = false;
1421
+ let runStartedAt = Date.now();
1422
+ let lastRaw = false;
1423
+ let intervalId = null;
1424
+ const tick = () => {
1425
+ try {
1426
+ analyser.getFloatTimeDomainData(buffer);
1427
+ } catch {
1428
+ return;
1429
+ }
1430
+ const rawSilent = isStreamSilent(buffer);
1431
+ const now = Date.now();
1432
+ if (rawSilent !== lastRaw) {
1433
+ lastRaw = rawSilent;
1434
+ runStartedAt = now;
1435
+ }
1436
+ if (rawSilent !== reportedSilent && now - runStartedAt >= SILENCE_SUSTAINED_MS) {
1437
+ reportedSilent = rawSilent;
1438
+ onChange(reportedSilent);
1439
+ }
1440
+ };
1441
+ intervalId = setInterval(tick, SILENCE_POLL_MS);
1442
+ return {
1443
+ stop() {
1444
+ if (intervalId !== null) {
1445
+ clearInterval(intervalId);
1446
+ intervalId = null;
1447
+ }
1448
+ try {
1449
+ source.disconnect();
1450
+ analyser.disconnect();
1451
+ } catch {
1452
+ }
1453
+ try {
1454
+ void audioCtx.close();
1455
+ } catch {
1456
+ }
1457
+ }
1458
+ };
1459
+ }
1372
1460
  async function startRecording(store, ctx) {
1373
1461
  store.micAcquiring = true;
1374
1462
  store.micFailReason = null;
1463
+ if (store.silenceMonitor) {
1464
+ store.silenceMonitor.stop();
1465
+ store.silenceMonitor = null;
1466
+ }
1467
+ store.micSilent = false;
1375
1468
  renderIndicatorState(store);
1376
1469
  if (!isMediaRecorderSupported()) {
1377
1470
  ctx.logger.warn("MediaRecorder not supported, continuing without audio");
@@ -1426,6 +1519,17 @@ async function startRecording(store, ctx) {
1426
1519
  store.indicatorState = "recording";
1427
1520
  }
1428
1521
  renderIndicatorState(store);
1522
+ const monitor = startSilenceMonitor(
1523
+ stream,
1524
+ (silent) => {
1525
+ const effectiveSilent = silent && !store.muted;
1526
+ if (store.micSilent === effectiveSilent) return;
1527
+ store.micSilent = effectiveSilent;
1528
+ renderIndicatorState(store);
1529
+ },
1530
+ ctx.logger
1531
+ );
1532
+ store.silenceMonitor = monitor;
1429
1533
  }
1430
1534
  function toggleMute(store) {
1431
1535
  if (!store.stream || !store.hasMicPermission) return false;
@@ -1456,6 +1560,11 @@ function flushMuteIfActive(store) {
1456
1560
  store.mutedSinceMs = null;
1457
1561
  }
1458
1562
  function stopRecording(store) {
1563
+ if (store.silenceMonitor) {
1564
+ store.silenceMonitor.stop();
1565
+ store.silenceMonitor = null;
1566
+ }
1567
+ store.micSilent = false;
1459
1568
  const recorder = store.recorder;
1460
1569
  if (recorder && recorder.state !== "inactive") {
1461
1570
  try {
@@ -1587,6 +1696,8 @@ function userTest(options = {}) {
1587
1696
  muted: false,
1588
1697
  mutedSinceMs: null,
1589
1698
  mutedSegments: [],
1699
+ micSilent: false,
1700
+ silenceMonitor: null,
1590
1701
  muteToastShown: false,
1591
1702
  muteToastTimers: [],
1592
1703
  notes: [],
@@ -1614,9 +1725,16 @@ function userTest(options = {}) {
1614
1725
  }
1615
1726
  return;
1616
1727
  }
1728
+ if (store.micSilent && !store.micAcquiring && store.indicatorState !== "finishing" && store.indicatorState !== "done" && store.indicatorState !== "error") {
1729
+ void startRecording(store, ctx);
1730
+ return;
1731
+ }
1617
1732
  const ok = toggleMute(store);
1618
1733
  if (!ok) return;
1619
- if (store.muted) showMuteToast(store);
1734
+ if (store.muted) {
1735
+ store.micSilent = false;
1736
+ showMuteToast(store);
1737
+ }
1620
1738
  renderIndicatorState(store);
1621
1739
  };
1622
1740
  const closeNote = () => closeNotePopover(store);
@@ -1752,8 +1870,17 @@ function userTest(options = {}) {
1752
1870
  }
1753
1871
  };
1754
1872
  }
1755
- var __test__ = { getTestSlug, pickMimeType, isMediaRecorderSupported, micChipState };
1873
+ var __test__ = {
1874
+ getTestSlug,
1875
+ pickMimeType,
1876
+ isMediaRecorderSupported,
1877
+ micChipState,
1878
+ isStreamSilent,
1879
+ rmsDbFromSamples,
1880
+ SILENCE_RMS_DB_THRESHOLD,
1881
+ SILENCE_FLOOR_DB
1882
+ };
1756
1883
 
1757
- export { __test__, userTest };
1884
+ export { __test__, isStreamSilent, rmsDbFromSamples, userTest };
1758
1885
  //# sourceMappingURL=user-test.js.map
1759
1886
  //# sourceMappingURL=user-test.js.map