@usero/sdk 1.1.9 → 1.1.10

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.
@@ -727,7 +727,9 @@ function micChipState(store) {
727
727
  if (store.micAcquiring) return "connecting";
728
728
  return "none";
729
729
  }
730
- return store.muted ? "muted" : "recording";
730
+ if (store.muted) return "muted";
731
+ if (store.micSilent) return "silent";
732
+ return "recording";
731
733
  }
732
734
  function renderIndicatorState(store) {
733
735
  const root = store.indicatorRoot;
@@ -740,7 +742,8 @@ function renderIndicatorState(store) {
740
742
  if (!(dot instanceof HTMLElement) || !mic || !(micIcon instanceof HTMLElement) || !(micLabel instanceof HTMLElement) || !btn) return;
741
743
  dot.setAttribute("data-state", store.indicatorState);
742
744
  const chipState = micChipState(store);
743
- mic.setAttribute("data-mic-state", chipState === "inactive" ? "none" : chipState);
745
+ const micStateAttr = chipState === "inactive" || chipState === "silent" ? "none" : chipState;
746
+ mic.setAttribute("data-mic-state", micStateAttr);
744
747
  mic.removeAttribute("data-mic-fail");
745
748
  if (chipState === "none") mic.setAttribute("data-mic-fail", store.micFailReason ?? "blocked");
746
749
  switch (store.indicatorState) {
@@ -784,6 +787,13 @@ function renderIndicatorState(store) {
784
787
  mic.setAttribute("aria-pressed", "false");
785
788
  mic.setAttribute("tabindex", "-1");
786
789
  break;
790
+ case "silent":
791
+ micIcon.innerHTML = MIC_MUTED_ICON_SVG;
792
+ micLabel.textContent = "We can't hear you, tap to recheck";
793
+ mic.setAttribute("aria-label", "We can't hear your microphone. Check your input device, then tap to recheck. Recording continues.");
794
+ mic.setAttribute("aria-pressed", "false");
795
+ mic.removeAttribute("tabindex");
796
+ break;
787
797
  case "none": {
788
798
  micIcon.innerHTML = MIC_MUTED_ICON_SVG;
789
799
  const failLabel = store.micFailReason === "not-found" ? "No mic found, tap to retry" : "Mic blocked, tap to retry";
@@ -1371,9 +1381,92 @@ function enqueueChunk(store, ctx, blob) {
1371
1381
  store.pendingUploads -= 1;
1372
1382
  });
1373
1383
  }
1384
+ var SILENCE_RMS_DB_THRESHOLD = -60;
1385
+ var SILENCE_FLOOR_DB = -100;
1386
+ var SILENCE_SUSTAINED_MS = 1800;
1387
+ var SILENCE_POLL_MS = 250;
1388
+ function rmsDbFromSamples(samples) {
1389
+ const n = samples.length;
1390
+ if (n === 0) return SILENCE_FLOOR_DB;
1391
+ let sumSquares = 0;
1392
+ for (let i = 0; i < n; i += 1) {
1393
+ const s = samples[i] ?? 0;
1394
+ sumSquares += s * s;
1395
+ }
1396
+ const rms = Math.sqrt(sumSquares / n);
1397
+ if (rms <= 0) return SILENCE_FLOOR_DB;
1398
+ const db = 20 * Math.log10(rms);
1399
+ return db < SILENCE_FLOOR_DB ? SILENCE_FLOOR_DB : db;
1400
+ }
1401
+ function isStreamSilent(input) {
1402
+ const rmsDb = typeof input === "number" ? input : rmsDbFromSamples(input);
1403
+ return rmsDb <= SILENCE_RMS_DB_THRESHOLD;
1404
+ }
1405
+ function startSilenceMonitor(stream, onChange, logger) {
1406
+ const Ctor = typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : void 0;
1407
+ if (!Ctor) return null;
1408
+ let audioCtx;
1409
+ let source;
1410
+ let analyser;
1411
+ try {
1412
+ audioCtx = new Ctor();
1413
+ source = audioCtx.createMediaStreamSource(stream);
1414
+ analyser = audioCtx.createAnalyser();
1415
+ analyser.fftSize = 2048;
1416
+ source.connect(analyser);
1417
+ } catch (err) {
1418
+ logger.warn("silence monitor: failed to attach analyser", err);
1419
+ return null;
1420
+ }
1421
+ const buffer = new Float32Array(analyser.fftSize);
1422
+ let reportedSilent = false;
1423
+ let runStartedAt = Date.now();
1424
+ let lastRaw = false;
1425
+ let intervalId = null;
1426
+ const tick = () => {
1427
+ try {
1428
+ analyser.getFloatTimeDomainData(buffer);
1429
+ } catch {
1430
+ return;
1431
+ }
1432
+ const rawSilent = isStreamSilent(buffer);
1433
+ const now = Date.now();
1434
+ if (rawSilent !== lastRaw) {
1435
+ lastRaw = rawSilent;
1436
+ runStartedAt = now;
1437
+ }
1438
+ if (rawSilent !== reportedSilent && now - runStartedAt >= SILENCE_SUSTAINED_MS) {
1439
+ reportedSilent = rawSilent;
1440
+ onChange(reportedSilent);
1441
+ }
1442
+ };
1443
+ intervalId = setInterval(tick, SILENCE_POLL_MS);
1444
+ return {
1445
+ stop() {
1446
+ if (intervalId !== null) {
1447
+ clearInterval(intervalId);
1448
+ intervalId = null;
1449
+ }
1450
+ try {
1451
+ source.disconnect();
1452
+ analyser.disconnect();
1453
+ } catch {
1454
+ }
1455
+ try {
1456
+ void audioCtx.close();
1457
+ } catch {
1458
+ }
1459
+ }
1460
+ };
1461
+ }
1374
1462
  async function startRecording(store, ctx) {
1375
1463
  store.micAcquiring = true;
1376
1464
  store.micFailReason = null;
1465
+ if (store.silenceMonitor) {
1466
+ store.silenceMonitor.stop();
1467
+ store.silenceMonitor = null;
1468
+ }
1469
+ store.micSilent = false;
1377
1470
  renderIndicatorState(store);
1378
1471
  if (!isMediaRecorderSupported()) {
1379
1472
  ctx.logger.warn("MediaRecorder not supported, continuing without audio");
@@ -1428,6 +1521,17 @@ async function startRecording(store, ctx) {
1428
1521
  store.indicatorState = "recording";
1429
1522
  }
1430
1523
  renderIndicatorState(store);
1524
+ const monitor = startSilenceMonitor(
1525
+ stream,
1526
+ (silent) => {
1527
+ const effectiveSilent = silent && !store.muted;
1528
+ if (store.micSilent === effectiveSilent) return;
1529
+ store.micSilent = effectiveSilent;
1530
+ renderIndicatorState(store);
1531
+ },
1532
+ ctx.logger
1533
+ );
1534
+ store.silenceMonitor = monitor;
1431
1535
  }
1432
1536
  function toggleMute(store) {
1433
1537
  if (!store.stream || !store.hasMicPermission) return false;
@@ -1458,6 +1562,11 @@ function flushMuteIfActive(store) {
1458
1562
  store.mutedSinceMs = null;
1459
1563
  }
1460
1564
  function stopRecording(store) {
1565
+ if (store.silenceMonitor) {
1566
+ store.silenceMonitor.stop();
1567
+ store.silenceMonitor = null;
1568
+ }
1569
+ store.micSilent = false;
1461
1570
  const recorder = store.recorder;
1462
1571
  if (recorder && recorder.state !== "inactive") {
1463
1572
  try {
@@ -1589,6 +1698,8 @@ function userTest(options = {}) {
1589
1698
  muted: false,
1590
1699
  mutedSinceMs: null,
1591
1700
  mutedSegments: [],
1701
+ micSilent: false,
1702
+ silenceMonitor: null,
1592
1703
  muteToastShown: false,
1593
1704
  muteToastTimers: [],
1594
1705
  notes: [],
@@ -1616,9 +1727,16 @@ function userTest(options = {}) {
1616
1727
  }
1617
1728
  return;
1618
1729
  }
1730
+ if (store.micSilent && !store.micAcquiring && store.indicatorState !== "finishing" && store.indicatorState !== "done" && store.indicatorState !== "error") {
1731
+ void startRecording(store, ctx);
1732
+ return;
1733
+ }
1619
1734
  const ok = toggleMute(store);
1620
1735
  if (!ok) return;
1621
- if (store.muted) showMuteToast(store);
1736
+ if (store.muted) {
1737
+ store.micSilent = false;
1738
+ showMuteToast(store);
1739
+ }
1622
1740
  renderIndicatorState(store);
1623
1741
  };
1624
1742
  const closeNote = () => closeNotePopover(store);
@@ -1754,9 +1872,20 @@ function userTest(options = {}) {
1754
1872
  }
1755
1873
  };
1756
1874
  }
1757
- var __test__ = { getTestSlug, pickMimeType, isMediaRecorderSupported, micChipState };
1875
+ var __test__ = {
1876
+ getTestSlug,
1877
+ pickMimeType,
1878
+ isMediaRecorderSupported,
1879
+ micChipState,
1880
+ isStreamSilent,
1881
+ rmsDbFromSamples,
1882
+ SILENCE_RMS_DB_THRESHOLD,
1883
+ SILENCE_FLOOR_DB
1884
+ };
1758
1885
 
1759
1886
  exports.__test__ = __test__;
1887
+ exports.isStreamSilent = isStreamSilent;
1888
+ exports.rmsDbFromSamples = rmsDbFromSamples;
1760
1889
  exports.userTest = userTest;
1761
1890
  //# sourceMappingURL=user-test.cjs.map
1762
1891
  //# sourceMappingURL=user-test.cjs.map