tempest-react-sdk 0.35.0 → 0.36.0

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.
Files changed (211) hide show
  1. package/README.md +72 -70
  2. package/dist/audio/audio-output.cjs +2 -0
  3. package/dist/audio/audio-output.cjs.map +1 -0
  4. package/dist/audio/audio-output.js +17 -0
  5. package/dist/audio/audio-output.js.map +1 -0
  6. package/dist/audio/audio-player.cjs +1 -1
  7. package/dist/audio/audio-player.cjs.map +1 -1
  8. package/dist/audio/audio-player.js +21 -20
  9. package/dist/audio/audio-player.js.map +1 -1
  10. package/dist/audio/audio-recorder.cjs +2 -0
  11. package/dist/audio/audio-recorder.cjs.map +1 -0
  12. package/dist/audio/audio-recorder.js +30 -0
  13. package/dist/audio/audio-recorder.js.map +1 -0
  14. package/dist/audio/duration.cjs +2 -0
  15. package/dist/audio/duration.cjs.map +1 -0
  16. package/dist/audio/duration.js +10 -0
  17. package/dist/audio/duration.js.map +1 -0
  18. package/dist/audio/level-meter.cjs +2 -0
  19. package/dist/audio/level-meter.cjs.map +1 -0
  20. package/dist/audio/level-meter.js +34 -0
  21. package/dist/audio/level-meter.js.map +1 -0
  22. package/dist/audio/media-access.cjs +2 -0
  23. package/dist/audio/media-access.cjs.map +1 -0
  24. package/dist/audio/media-access.js +51 -0
  25. package/dist/audio/media-access.js.map +1 -0
  26. package/dist/audio/use-audio-recorder.cjs +2 -0
  27. package/dist/audio/use-audio-recorder.cjs.map +1 -0
  28. package/dist/audio/use-audio-recorder.js +94 -0
  29. package/dist/audio/use-audio-recorder.js.map +1 -0
  30. package/dist/audio/use-audio.cjs.map +1 -1
  31. package/dist/audio/use-audio.js.map +1 -1
  32. package/dist/audio/use-microphone.cjs +2 -0
  33. package/dist/audio/use-microphone.cjs.map +1 -0
  34. package/dist/audio/use-microphone.js +54 -0
  35. package/dist/audio/use-microphone.js.map +1 -0
  36. package/dist/audio/wav.cjs +2 -0
  37. package/dist/audio/wav.cjs.map +1 -0
  38. package/dist/audio/wav.js +49 -0
  39. package/dist/audio/wav.js.map +1 -0
  40. package/dist/auth/passkey.cjs +2 -0
  41. package/dist/auth/passkey.cjs.map +1 -0
  42. package/dist/auth/passkey.js +171 -0
  43. package/dist/auth/passkey.js.map +1 -0
  44. package/dist/auth/use-passkey.cjs +2 -0
  45. package/dist/auth/use-passkey.cjs.map +1 -0
  46. package/dist/auth/use-passkey.js +120 -0
  47. package/dist/auth/use-passkey.js.map +1 -0
  48. package/dist/br/PixQRCode.cjs +2 -0
  49. package/dist/br/PixQRCode.cjs.map +1 -0
  50. package/dist/br/PixQRCode.js +70 -0
  51. package/dist/br/PixQRCode.js.map +1 -0
  52. package/dist/br/PixQRCode.module.cjs +2 -0
  53. package/dist/br/PixQRCode.module.cjs.map +1 -0
  54. package/dist/br/PixQRCode.module.js +15 -0
  55. package/dist/br/PixQRCode.module.js.map +1 -0
  56. package/dist/br/boleto.cjs +2 -0
  57. package/dist/br/boleto.cjs.map +1 -0
  58. package/dist/br/boleto.js +225 -0
  59. package/dist/br/boleto.js.map +1 -0
  60. package/dist/br/holidays.cjs +2 -0
  61. package/dist/br/holidays.cjs.map +1 -0
  62. package/dist/br/holidays.js +148 -0
  63. package/dist/br/holidays.js.map +1 -0
  64. package/dist/br/nfe.cjs +2 -0
  65. package/dist/br/nfe.cjs.map +1 -0
  66. package/dist/br/nfe.js +104 -0
  67. package/dist/br/nfe.js.map +1 -0
  68. package/dist/br/pix.cjs +2 -0
  69. package/dist/br/pix.cjs.map +1 -0
  70. package/dist/br/pix.js +148 -0
  71. package/dist/br/pix.js.map +1 -0
  72. package/dist/br.cjs +1 -1
  73. package/dist/br.cjs.map +1 -1
  74. package/dist/br.d.ts +878 -0
  75. package/dist/br.js +7 -2
  76. package/dist/br.js.map +1 -1
  77. package/dist/capture/barcode.cjs +2 -0
  78. package/dist/capture/barcode.cjs.map +1 -0
  79. package/dist/capture/barcode.js +59 -0
  80. package/dist/capture/barcode.js.map +1 -0
  81. package/dist/capture/media-recorder.cjs +2 -0
  82. package/dist/capture/media-recorder.cjs.map +1 -0
  83. package/dist/capture/media-recorder.js +64 -0
  84. package/dist/capture/media-recorder.js.map +1 -0
  85. package/dist/capture/use-barcode-scanner.cjs +2 -0
  86. package/dist/capture/use-barcode-scanner.cjs.map +1 -0
  87. package/dist/capture/use-barcode-scanner.js +86 -0
  88. package/dist/capture/use-barcode-scanner.js.map +1 -0
  89. package/dist/capture/use-screen-capture.cjs +2 -0
  90. package/dist/capture/use-screen-capture.cjs.map +1 -0
  91. package/dist/capture/use-screen-capture.js +79 -0
  92. package/dist/capture/use-screen-capture.js.map +1 -0
  93. package/dist/capture/use-speech-recognition.cjs +2 -0
  94. package/dist/capture/use-speech-recognition.cjs.map +1 -0
  95. package/dist/capture/use-speech-recognition.js +118 -0
  96. package/dist/capture/use-speech-recognition.js.map +1 -0
  97. package/dist/capture/use-torch.cjs +2 -0
  98. package/dist/capture/use-torch.cjs.map +1 -0
  99. package/dist/capture/use-torch.js +33 -0
  100. package/dist/capture/use-torch.js.map +1 -0
  101. package/dist/capture/use-video-recorder.cjs +2 -0
  102. package/dist/capture/use-video-recorder.cjs.map +1 -0
  103. package/dist/capture/use-video-recorder.js +90 -0
  104. package/dist/capture/use-video-recorder.js.map +1 -0
  105. package/dist/capture/video-recorder.cjs +2 -0
  106. package/dist/capture/video-recorder.cjs.map +1 -0
  107. package/dist/capture/video-recorder.js +31 -0
  108. package/dist/capture/video-recorder.js.map +1 -0
  109. package/dist/components/AIChat/AIChat.cjs +1 -1
  110. package/dist/components/AIChat/AIChat.cjs.map +1 -1
  111. package/dist/components/AIChat/AIChat.js +93 -97
  112. package/dist/components/AIChat/AIChat.js.map +1 -1
  113. package/dist/components/AIChat/AIChatComposer.cjs +1 -1
  114. package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
  115. package/dist/components/AIChat/AIChatComposer.js +2 -1
  116. package/dist/components/AIChat/AIChatComposer.js.map +1 -1
  117. package/dist/components/AIChat/AIChatTurn.cjs +2 -2
  118. package/dist/components/AIChat/AIChatTurn.js +6 -6
  119. package/dist/components/AIChat/AIChatTurn.js.map +1 -1
  120. package/dist/components/Alert/Alert.module.cjs.map +1 -1
  121. package/dist/components/Alert/Alert.module.js.map +1 -1
  122. package/dist/components/AudioPlayer/AudioPlayer.cjs +2 -0
  123. package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -0
  124. package/dist/components/AudioPlayer/AudioPlayer.js +123 -0
  125. package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -0
  126. package/dist/components/AudioPlayer/AudioPlayer.module.cjs +2 -0
  127. package/dist/components/AudioPlayer/AudioPlayer.module.cjs.map +1 -0
  128. package/dist/components/AudioPlayer/AudioPlayer.module.js +12 -0
  129. package/dist/components/AudioPlayer/AudioPlayer.module.js.map +1 -0
  130. package/dist/components/AudioRecorder/AudioRecorder.cjs +2 -0
  131. package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -0
  132. package/dist/components/AudioRecorder/AudioRecorder.js +179 -0
  133. package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -0
  134. package/dist/components/AudioRecorder/AudioRecorder.module.cjs +2 -0
  135. package/dist/components/AudioRecorder/AudioRecorder.module.cjs.map +1 -0
  136. package/dist/components/AudioRecorder/AudioRecorder.module.js +19 -0
  137. package/dist/components/AudioRecorder/AudioRecorder.module.js.map +1 -0
  138. package/dist/components/Badge/Badge.module.cjs.map +1 -1
  139. package/dist/components/Badge/Badge.module.js.map +1 -1
  140. package/dist/components/BarcodeScanner/BarcodeScanner.cjs +2 -0
  141. package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -0
  142. package/dist/components/BarcodeScanner/BarcodeScanner.js +123 -0
  143. package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -0
  144. package/dist/components/BarcodeScanner/BarcodeScanner.module.cjs +2 -0
  145. package/dist/components/BarcodeScanner/BarcodeScanner.module.cjs.map +1 -0
  146. package/dist/components/BarcodeScanner/BarcodeScanner.module.js +22 -0
  147. package/dist/components/BarcodeScanner/BarcodeScanner.module.js.map +1 -0
  148. package/dist/components/BottomNavigation/BottomNavigation.module.cjs.map +1 -1
  149. package/dist/components/BottomNavigation/BottomNavigation.module.js.map +1 -1
  150. package/dist/components/Button/Button.module.cjs.map +1 -1
  151. package/dist/components/Button/Button.module.js.map +1 -1
  152. package/dist/components/Combobox/Combobox.module.cjs.map +1 -1
  153. package/dist/components/Combobox/Combobox.module.js.map +1 -1
  154. package/dist/components/DataTable/DataTable.cjs +1 -1
  155. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  156. package/dist/components/DataTable/DataTable.js +203 -66
  157. package/dist/components/DataTable/DataTable.js.map +1 -1
  158. package/dist/components/DataTable/DataTable.module.cjs +1 -1
  159. package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
  160. package/dist/components/DataTable/DataTable.module.js +9 -3
  161. package/dist/components/DataTable/DataTable.module.js.map +1 -1
  162. package/dist/components/DataTable/EditableCell.cjs +2 -0
  163. package/dist/components/DataTable/EditableCell.cjs.map +1 -0
  164. package/dist/components/DataTable/EditableCell.js +77 -0
  165. package/dist/components/DataTable/EditableCell.js.map +1 -0
  166. package/dist/components/DataTable/edit-labels.cjs +2 -0
  167. package/dist/components/DataTable/edit-labels.cjs.map +1 -0
  168. package/dist/components/DataTable/edit-labels.js +11 -0
  169. package/dist/components/DataTable/edit-labels.js.map +1 -0
  170. package/dist/components/DateRangePicker/DateRangePicker.module.cjs.map +1 -1
  171. package/dist/components/DateRangePicker/DateRangePicker.module.js.map +1 -1
  172. package/dist/components/MultiSelect/MultiSelect.module.cjs.map +1 -1
  173. package/dist/components/MultiSelect/MultiSelect.module.js.map +1 -1
  174. package/dist/components/NavigationRail/NavigationRail.module.cjs.map +1 -1
  175. package/dist/components/NavigationRail/NavigationRail.module.js.map +1 -1
  176. package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs +1 -1
  177. package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs.map +1 -1
  178. package/dist/components/SyncStatusBadge/SyncStatusBadge.js +33 -32
  179. package/dist/components/SyncStatusBadge/SyncStatusBadge.js.map +1 -1
  180. package/dist/components/Toast/ToastProvider.cjs +1 -1
  181. package/dist/components/Toast/ToastProvider.cjs.map +1 -1
  182. package/dist/components/Toast/ToastProvider.js +1 -2
  183. package/dist/components/Toast/ToastProvider.js.map +1 -1
  184. package/dist/hooks/use-announce.cjs +2 -0
  185. package/dist/hooks/use-announce.cjs.map +1 -0
  186. package/dist/hooks/use-announce.js +50 -0
  187. package/dist/hooks/use-announce.js.map +1 -0
  188. package/dist/hooks/use-media-devices.cjs +2 -0
  189. package/dist/hooks/use-media-devices.cjs.map +1 -0
  190. package/dist/hooks/use-media-devices.js +46 -0
  191. package/dist/hooks/use-media-devices.js.map +1 -0
  192. package/dist/hooks/use-media-permission.cjs +2 -0
  193. package/dist/hooks/use-media-permission.cjs.map +1 -0
  194. package/dist/hooks/use-media-permission.js +33 -0
  195. package/dist/hooks/use-media-permission.js.map +1 -0
  196. package/dist/http/resumable-upload.cjs +2 -0
  197. package/dist/http/resumable-upload.cjs.map +1 -0
  198. package/dist/http/resumable-upload.js +249 -0
  199. package/dist/http/resumable-upload.js.map +1 -0
  200. package/dist/styles.css +1 -1
  201. package/dist/tempest-react-sdk.cjs +1 -1
  202. package/dist/tempest-react-sdk.d.ts +2485 -29
  203. package/dist/tempest-react-sdk.js +268 -244
  204. package/dist/vision/use-camera-stream.cjs +1 -1
  205. package/dist/vision/use-camera-stream.cjs.map +1 -1
  206. package/dist/vision/use-camera-stream.js +40 -57
  207. package/dist/vision/use-camera-stream.js.map +1 -1
  208. package/dist/vision.cjs +1 -1
  209. package/dist/vision.d.ts +25 -3
  210. package/dist/vision.js +19 -19
  211. package/package.json +1 -1
@@ -1,35 +1,36 @@
1
+ import { setAudioOutput as e } from "./audio-output.js";
1
2
  //#region src/audio/audio-player.ts
2
- function e() {
3
- let e = null;
4
- async function t(t, { volume: n = 1, loop: r = !1, autoplay: i = !0, stopPrevious: a = !1, onEnded: o, onError: s } = {}) {
3
+ function t() {
4
+ let t = null;
5
+ async function n(n, { volume: r = 1, loop: i = !1, autoplay: a = !0, stopPrevious: o = !1, sinkId: s, onEnded: c, onError: l } = {}) {
5
6
  try {
6
- a && e && (e.pause(), e.currentTime = 0);
7
- let c = new Audio(t);
8
- return c.volume = Math.max(0, Math.min(1, n)), c.loop = r, c.preload = "auto", o && (c.onended = o), s && (c.onerror = (e) => s(e)), e = c, i && await c.play(), c;
7
+ o && t && (t.pause(), t.currentTime = 0);
8
+ let u = new Audio(n);
9
+ return u.volume = Math.max(0, Math.min(1, r)), u.loop = i, u.preload = "auto", c && (u.onended = c), l && (u.onerror = (e) => l(e)), t = u, s !== void 0 && await e(u, s), a && await u.play(), u;
9
10
  } catch (e) {
10
- return s?.(e), null;
11
+ return l?.(e), null;
11
12
  }
12
13
  }
13
- function n() {
14
- e && (e.pause(), e.currentTime = 0);
14
+ function r() {
15
+ t && (t.pause(), t.currentTime = 0);
15
16
  }
16
17
  return {
17
- play: t,
18
- stop: n,
19
- current: () => e
18
+ play: n,
19
+ stop: r,
20
+ current: () => t
20
21
  };
21
22
  }
22
- var t = null;
23
- function n() {
24
- return t ||= e(), t;
23
+ var n = null;
24
+ function r() {
25
+ return n ||= t(), n;
25
26
  }
26
- async function r(e, t) {
27
- return n().play(e, t);
27
+ async function i(e, t) {
28
+ return r().play(e, t);
28
29
  }
29
- function i() {
30
- n().stop();
30
+ function a() {
31
+ r().stop();
31
32
  }
32
33
  //#endregion
33
- export { e as createAudioPlayer, r as playAudio, i as stopAudio };
34
+ export { t as createAudioPlayer, i as playAudio, a as stopAudio };
34
35
 
35
36
  //# sourceMappingURL=audio-player.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"audio-player.js","names":[],"sources":["../../src/audio/audio-player.ts"],"sourcesContent":["export interface PlayAudioOptions {\n /** Volume between 0 and 1. Default: 1. */\n volume?: number;\n /** Loop the clip. Default: false. */\n loop?: boolean;\n /** Begin playback immediately. Default: true. */\n autoplay?: boolean;\n /** Stop the previous clip managed by this player. Default: false. */\n stopPrevious?: boolean;\n /** Fired when playback ends naturally. */\n onEnded?: () => void;\n /** Fired on playback error. */\n onError?: (error: unknown) => void;\n}\n\nexport interface AudioPlayer {\n /** Play `src`. Returns the underlying element, or `null` when the browser blocked autoplay. */\n play: (src: string, options?: PlayAudioOptions) => Promise<HTMLAudioElement | null>;\n /** Stop the currently-playing clip and rewind it. */\n stop: () => void;\n /** Currently playing audio element, or `null`. */\n current: () => HTMLAudioElement | null;\n}\n\n/**\n * Create an isolated audio player that tracks a single \"current\" clip.\n * Multiple players coexist independently; use this when several layers of UI\n * need their own playback state.\n */\nexport function createAudioPlayer(): AudioPlayer {\n let current: HTMLAudioElement | null = null;\n\n async function play(\n src: string,\n {\n volume = 1,\n loop = false,\n autoplay = true,\n stopPrevious = false,\n onEnded,\n onError,\n }: PlayAudioOptions = {},\n ): Promise<HTMLAudioElement | null> {\n try {\n if (stopPrevious && current) {\n current.pause();\n current.currentTime = 0;\n }\n const audio = new Audio(src);\n audio.volume = Math.max(0, Math.min(1, volume));\n audio.loop = loop;\n audio.preload = \"auto\";\n if (onEnded) audio.onended = onEnded;\n if (onError) audio.onerror = (event) => onError(event);\n current = audio;\n if (autoplay) await audio.play();\n return audio;\n } catch (error) {\n onError?.(error);\n return null;\n }\n }\n\n function stop(): void {\n if (!current) return;\n current.pause();\n current.currentTime = 0;\n }\n\n return { play, stop, current: () => current };\n}\n\nlet defaultPlayer: AudioPlayer | null = null;\n\nfunction getDefaultPlayer(): AudioPlayer {\n if (!defaultPlayer) defaultPlayer = createAudioPlayer();\n return defaultPlayer;\n}\n\n/**\n * Convenience wrapper around a shared {@link AudioPlayer}. Use this for\n * one-off notification sounds. For more complex flows (e.g. several\n * simultaneous channels), build a dedicated player with {@link createAudioPlayer}.\n */\nexport async function playAudio(\n src: string,\n options?: PlayAudioOptions,\n): Promise<HTMLAudioElement | null> {\n return getDefaultPlayer().play(src, options);\n}\n\n/** Stop the clip currently playing on the shared default player. */\nexport function stopAudio(): void {\n getDefaultPlayer().stop();\n}\n"],"mappings":";AA6BA,SAAgB,IAAiC;CAC7C,IAAI,IAAmC;CAEvC,eAAe,EACX,GACA,EACI,YAAS,GACT,UAAO,IACP,cAAW,IACX,kBAAe,IACf,YACA,eACkB,CAAC,GACS;EAChC,IAAI;GACA,AAAI,KAAgB,MAChB,EAAQ,MAAM,GACd,EAAQ,cAAc;GAE1B,IAAM,IAAQ,IAAI,MAAM,CAAG;GAQ3B,OAPA,EAAM,SAAS,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,CAAM,CAAC,GAC9C,EAAM,OAAO,GACb,EAAM,UAAU,QACZ,MAAS,EAAM,UAAU,IACzB,MAAS,EAAM,WAAW,MAAU,EAAQ,CAAK,IACrD,IAAU,GACN,KAAU,MAAM,EAAM,KAAK,GACxB;EACX,SAAS,GAAO;GAEZ,OADA,IAAU,CAAK,GACR;EACX;CACJ;CAEA,SAAS,IAAa;EACb,MACL,EAAQ,MAAM,GACd,EAAQ,cAAc;CAC1B;CAEA,OAAO;EAAE;EAAM;EAAM,eAAe;CAAQ;AAChD;AAEA,IAAI,IAAoC;AAExC,SAAS,IAAgC;CAErC,OADA,AAAoB,MAAgB,EAAkB,GAC/C;AACX;AAOA,eAAsB,EAClB,GACA,GACgC;CAChC,OAAO,EAAiB,CAAC,CAAC,KAAK,GAAK,CAAO;AAC/C;AAGA,SAAgB,IAAkB;CAC9B,EAAiB,CAAC,CAAC,KAAK;AAC5B"}
1
+ {"version":3,"file":"audio-player.js","names":[],"sources":["../../src/audio/audio-player.ts"],"sourcesContent":["import { setAudioOutput } from \"./audio-output\";\n\nexport interface PlayAudioOptions {\n /** Volume between 0 and 1. Default: 1. */\n volume?: number;\n /** Loop the clip. Default: false. */\n loop?: boolean;\n /** Begin playback immediately. Default: true. */\n autoplay?: boolean;\n /** Stop the previous clip managed by this player. Default: false. */\n stopPrevious?: boolean;\n /**\n * Output device to play on, from `useMediaDevices().audioOutputs`.\n *\n * Ignored where the browser has no output routing (everything but Chromium) —\n * the clip still plays, on the system default. Useful for a chime that must land\n * on a headset while the call audio stays on the speakers.\n */\n sinkId?: string;\n /** Fired when playback ends naturally. */\n onEnded?: () => void;\n /** Fired on playback error. */\n onError?: (error: unknown) => void;\n}\n\n/**\n * Imperative handle over one \"current\" clip.\n *\n * Named `*Handle` like the SDK's other imperative handles (`ChatComposerHandle`,\n * `SignaturePadHandle`) — it is a control surface, not the `<AudioPlayer>` component.\n */\nexport interface AudioPlayerHandle {\n /** Play `src`. Returns the underlying element, or `null` when the browser blocked autoplay. */\n play: (src: string, options?: PlayAudioOptions) => Promise<HTMLAudioElement | null>;\n /** Stop the currently-playing clip and rewind it. */\n stop: () => void;\n /** Currently playing audio element, or `null`. */\n current: () => HTMLAudioElement | null;\n}\n\n/**\n * Create an isolated audio player that tracks a single \"current\" clip.\n * Multiple players coexist independently; use this when several layers of UI\n * need their own playback state.\n */\nexport function createAudioPlayer(): AudioPlayerHandle {\n let current: HTMLAudioElement | null = null;\n\n async function play(\n src: string,\n {\n volume = 1,\n loop = false,\n autoplay = true,\n stopPrevious = false,\n sinkId,\n onEnded,\n onError,\n }: PlayAudioOptions = {},\n ): Promise<HTMLAudioElement | null> {\n try {\n if (stopPrevious && current) {\n current.pause();\n current.currentTime = 0;\n }\n const audio = new Audio(src);\n audio.volume = Math.max(0, Math.min(1, volume));\n audio.loop = loop;\n audio.preload = \"auto\";\n if (onEnded) audio.onended = onEnded;\n if (onError) audio.onerror = (event) => onError(event);\n current = audio;\n // Routed before `play()`: applying a sink to an element that is already\n // playing restarts its audio pipeline and clips the first few ms.\n if (sinkId !== undefined) await setAudioOutput(audio, sinkId);\n if (autoplay) await audio.play();\n return audio;\n } catch (error) {\n onError?.(error);\n return null;\n }\n }\n\n function stop(): void {\n if (!current) return;\n current.pause();\n current.currentTime = 0;\n }\n\n return { play, stop, current: () => current };\n}\n\nlet defaultPlayer: AudioPlayerHandle | null = null;\n\nfunction getDefaultPlayer(): AudioPlayerHandle {\n if (!defaultPlayer) defaultPlayer = createAudioPlayer();\n return defaultPlayer;\n}\n\n/**\n * Convenience wrapper around a shared {@link AudioPlayerHandle}. Use this for\n * one-off notification sounds. For more complex flows (e.g. several\n * simultaneous channels), build a dedicated player with {@link createAudioPlayer}.\n */\nexport async function playAudio(\n src: string,\n options?: PlayAudioOptions,\n): Promise<HTMLAudioElement | null> {\n return getDefaultPlayer().play(src, options);\n}\n\n/** Stop the clip currently playing on the shared default player. */\nexport function stopAudio(): void {\n getDefaultPlayer().stop();\n}\n"],"mappings":";;AA6CA,SAAgB,IAAuC;CACnD,IAAI,IAAmC;CAEvC,eAAe,EACX,GACA,EACI,YAAS,GACT,UAAO,IACP,cAAW,IACX,kBAAe,IACf,WACA,YACA,eACkB,CAAC,GACS;EAChC,IAAI;GACA,AAAI,KAAgB,MAChB,EAAQ,MAAM,GACd,EAAQ,cAAc;GAE1B,IAAM,IAAQ,IAAI,MAAM,CAAG;GAW3B,OAVA,EAAM,SAAS,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,CAAM,CAAC,GAC9C,EAAM,OAAO,GACb,EAAM,UAAU,QACZ,MAAS,EAAM,UAAU,IACzB,MAAS,EAAM,WAAW,MAAU,EAAQ,CAAK,IACrD,IAAU,GAGN,MAAW,KAAA,KAAW,MAAM,EAAe,GAAO,CAAM,GACxD,KAAU,MAAM,EAAM,KAAK,GACxB;EACX,SAAS,GAAO;GAEZ,OADA,IAAU,CAAK,GACR;EACX;CACJ;CAEA,SAAS,IAAa;EACb,MACL,EAAQ,MAAM,GACd,EAAQ,cAAc;CAC1B;CAEA,OAAO;EAAE;EAAM;EAAM,eAAe;CAAQ;AAChD;AAEA,IAAI,IAA0C;AAE9C,SAAS,IAAsC;CAE3C,OADA,AAAoB,MAAgB,EAAkB,GAC/C;AACX;AAOA,eAAsB,EAClB,GACA,GACgC;CAChC,OAAO,EAAiB,CAAC,CAAC,KAAK,GAAK,CAAO;AAC/C;AAGA,SAAgB,IAAkB;CAC9B,EAAiB,CAAC,CAAC,KAAK;AAC5B"}
@@ -0,0 +1,2 @@
1
+ const e=require("../capture/media-recorder.cjs");var t=[`audio/webm;codecs=opus`,`audio/webm`,`audio/ogg;codecs=opus`,`audio/mp4;codecs=mp4a.40.2`,`audio/mp4`];function n(){return typeof MediaRecorder<`u`&&r()!==null}function r(n=t){return e.pickRecordingMimeType(n)}function i(n,r={}){return e.createMediaRecorder(n,{candidates:t,kind:`audio`,mimeType:r.mimeType,audioBitsPerSecond:r.audioBitsPerSecond,timesliceMs:r.timesliceMs,onChunk:r.onChunk,onError:r.onError})}exports.AUDIO_MIME_CANDIDATES=t,exports.createAudioRecorder=i,exports.isAudioRecordingSupported=n,exports.pickAudioMimeType=r;
2
+ //# sourceMappingURL=audio-recorder.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-recorder.cjs","names":[],"sources":["../../src/audio/audio-recorder.ts"],"sourcesContent":["import {\n createMediaRecorder,\n pickRecordingMimeType,\n type MediaRecorderStatus,\n} from \"@/capture/media-recorder\";\n\n/** Lifecycle of a recording. Shared with the video recorder — the states are the same. */\nexport type AudioRecorderStatus = MediaRecorderStatus;\n\n/**\n * Container/codec candidates, best first.\n *\n * No browser supports all of these, and none supports MP3 or WAV from\n * `MediaRecorder` — see the note on {@link pickAudioMimeType}. Opus in WebM is the\n * first choice because it is the smallest at speech bitrates and is what Chromium\n * and Firefox produce natively; `audio/mp4` is here for Safari, which produces AAC\n * and nothing else.\n *\n * Ported from the intersection of what `MediaRecorder.isTypeSupported` accepts in\n * Chrome 120+, Firefox 115+ and Safari 17. Re-check against\n * <https://developer.mozilla.org/docs/Web/API/MediaRecorder/isTypeSupported> before\n * adding to the list.\n */\nexport const AUDIO_MIME_CANDIDATES: readonly string[] = [\n \"audio/webm;codecs=opus\",\n \"audio/webm\",\n \"audio/ogg;codecs=opus\",\n \"audio/mp4;codecs=mp4a.40.2\",\n \"audio/mp4\",\n];\n\n/** A finished recording. */\nexport interface AudioRecording {\n /** The audio. Wrap with `useObjectUrl` to play it, or POST it as-is. */\n blob: Blob;\n /** What the browser actually produced — not necessarily what you asked for. */\n mimeType: string;\n /** Recorded length, excluding time spent paused. */\n durationMs: number;\n}\n\n/** Options for {@link createAudioRecorder}. */\nexport interface AudioRecorderOptions {\n /**\n * Force a container. Throws when the browser cannot produce it.\n *\n * Leave it out. The default negotiates from {@link AUDIO_MIME_CANDIDATES}, which\n * is the only way one call site works on both Chromium and Safari.\n */\n mimeType?: string;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /**\n * Emit a chunk every N ms through `onChunk`, for streaming upload.\n *\n * Without it the whole recording is buffered in memory until `stop()` — fine for\n * a voice note, not fine for an hour-long meeting.\n */\n timesliceMs?: number;\n /** Receives each chunk when `timesliceMs` is set. Chunks are **not** independently playable. */\n onChunk?: (chunk: Blob) => void;\n /** Recorder-level failure (device unplugged mid-recording, encoder error). */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative recorder. */\nexport interface AudioRecorderHandle {\n /** Begin recording. No-op when already recording or paused. */\n start: () => void;\n /** Pause. The clock stops; `durationMs` freezes. */\n pause: () => void;\n /** Resume after `pause()`. */\n resume: () => void;\n /** Stop and resolve with the assembled recording. */\n stop: () => Promise<AudioRecording>;\n /** Stop and throw the audio away. */\n cancel: () => void;\n status: () => AudioRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: () => number;\n /** The negotiated container. */\n mimeType: string;\n}\n\n/** Whether `MediaRecorder` exists and can produce at least one audio container. */\nexport function isAudioRecordingSupported(): boolean {\n return typeof MediaRecorder !== \"undefined\" && pickAudioMimeType() !== null;\n}\n\n/**\n * First container in `preferred` the browser can actually produce, or `null`.\n *\n * **There is no MP3 or WAV here, and that is not an omission.** `MediaRecorder`\n * emits Opus (in WebM or Ogg) on Chromium and Firefox and AAC (in MP4) on Safari —\n * no engine implements an MP3 or WAV encoder for it. If a backend needs WAV,\n * {@link blobToWav} converts one client-side with no dependency; if it needs MP3,\n * transcode on the server. Shipping an MP3 encoder would mean a WASM build of the\n * order of 150 KB in every consumer's bundle to serve one format, which is the\n * trade this SDK does not make.\n *\n * @param preferred - Candidates, best first. Defaults to {@link AUDIO_MIME_CANDIDATES}.\n * @returns A supported MIME type, or `null` when none is.\n */\nexport function pickAudioMimeType(\n preferred: readonly string[] = AUDIO_MIME_CANDIDATES,\n): string | null {\n return pickRecordingMimeType(preferred);\n}\n\n/**\n * Wrap a `MediaStream` in a recorder with a real state machine and an honest clock.\n *\n * The engine is shared with the video recorder (`createMediaRecorder`), because the\n * subtle parts are not audio-specific: `MediaRecorder` reports **no duration** (WebM\n * it writes carries none in its header, which is why `<audio>` shows `Infinity` for a\n * fresh recording) so the clock is kept by hand and subtracts paused time; and\n * `stop()` returns *before* the last `dataavailable` event, so the blob can only be\n * assembled in `onstop`. What stays here is the part that genuinely differs — the\n * container list.\n *\n * The stream is **not** owned here: `stop()` leaves the microphone open so a retake\n * does not need a second permission round-trip. Release it with the owning\n * `useMicrophone().stop()`.\n *\n * @param stream - A live audio stream, usually from {@link useMicrophone}.\n * @param options - See {@link AudioRecorderOptions}.\n * @returns The imperative recorder.\n * @throws When `MediaRecorder` is unavailable, or an explicit `mimeType` is unsupported.\n */\nexport function createAudioRecorder(\n stream: MediaStream,\n options: AudioRecorderOptions = {},\n): AudioRecorderHandle {\n return createMediaRecorder(stream, {\n candidates: AUDIO_MIME_CANDIDATES,\n kind: \"audio\",\n mimeType: options.mimeType,\n audioBitsPerSecond: options.audioBitsPerSecond,\n timesliceMs: options.timesliceMs,\n onChunk: options.onChunk,\n onError: options.onError,\n });\n}\n"],"mappings":"iDAuBA,IAAa,EAA2C,CACpD,yBACA,aACA,wBACA,6BACA,WACJ,EAwDA,SAAgB,GAAqC,CACjD,OAAO,OAAO,cAAkB,KAAe,EAAkB,IAAM,IAC3E,CAgBA,SAAgB,EACZ,EAA+B,EAClB,CACb,OAAO,EAAA,sBAAsB,CAAS,CAC1C,CAsBA,SAAgB,EACZ,EACA,EAAgC,CAAC,EACd,CACnB,OAAO,EAAA,oBAAoB,EAAQ,CAC/B,WAAY,EACZ,KAAM,QACN,SAAU,EAAQ,SAClB,mBAAoB,EAAQ,mBAC5B,YAAa,EAAQ,YACrB,QAAS,EAAQ,QACjB,QAAS,EAAQ,OACrB,CAAC,CACL"}
@@ -0,0 +1,30 @@
1
+ import { createMediaRecorder as e, pickRecordingMimeType as t } from "../capture/media-recorder.js";
2
+ //#region src/audio/audio-recorder.ts
3
+ var n = [
4
+ "audio/webm;codecs=opus",
5
+ "audio/webm",
6
+ "audio/ogg;codecs=opus",
7
+ "audio/mp4;codecs=mp4a.40.2",
8
+ "audio/mp4"
9
+ ];
10
+ function r() {
11
+ return typeof MediaRecorder < "u" && i() !== null;
12
+ }
13
+ function i(e = n) {
14
+ return t(e);
15
+ }
16
+ function a(t, r = {}) {
17
+ return e(t, {
18
+ candidates: n,
19
+ kind: "audio",
20
+ mimeType: r.mimeType,
21
+ audioBitsPerSecond: r.audioBitsPerSecond,
22
+ timesliceMs: r.timesliceMs,
23
+ onChunk: r.onChunk,
24
+ onError: r.onError
25
+ });
26
+ }
27
+ //#endregion
28
+ export { n as AUDIO_MIME_CANDIDATES, a as createAudioRecorder, r as isAudioRecordingSupported, i as pickAudioMimeType };
29
+
30
+ //# sourceMappingURL=audio-recorder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-recorder.js","names":[],"sources":["../../src/audio/audio-recorder.ts"],"sourcesContent":["import {\n createMediaRecorder,\n pickRecordingMimeType,\n type MediaRecorderStatus,\n} from \"@/capture/media-recorder\";\n\n/** Lifecycle of a recording. Shared with the video recorder — the states are the same. */\nexport type AudioRecorderStatus = MediaRecorderStatus;\n\n/**\n * Container/codec candidates, best first.\n *\n * No browser supports all of these, and none supports MP3 or WAV from\n * `MediaRecorder` — see the note on {@link pickAudioMimeType}. Opus in WebM is the\n * first choice because it is the smallest at speech bitrates and is what Chromium\n * and Firefox produce natively; `audio/mp4` is here for Safari, which produces AAC\n * and nothing else.\n *\n * Ported from the intersection of what `MediaRecorder.isTypeSupported` accepts in\n * Chrome 120+, Firefox 115+ and Safari 17. Re-check against\n * <https://developer.mozilla.org/docs/Web/API/MediaRecorder/isTypeSupported> before\n * adding to the list.\n */\nexport const AUDIO_MIME_CANDIDATES: readonly string[] = [\n \"audio/webm;codecs=opus\",\n \"audio/webm\",\n \"audio/ogg;codecs=opus\",\n \"audio/mp4;codecs=mp4a.40.2\",\n \"audio/mp4\",\n];\n\n/** A finished recording. */\nexport interface AudioRecording {\n /** The audio. Wrap with `useObjectUrl` to play it, or POST it as-is. */\n blob: Blob;\n /** What the browser actually produced — not necessarily what you asked for. */\n mimeType: string;\n /** Recorded length, excluding time spent paused. */\n durationMs: number;\n}\n\n/** Options for {@link createAudioRecorder}. */\nexport interface AudioRecorderOptions {\n /**\n * Force a container. Throws when the browser cannot produce it.\n *\n * Leave it out. The default negotiates from {@link AUDIO_MIME_CANDIDATES}, which\n * is the only way one call site works on both Chromium and Safari.\n */\n mimeType?: string;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /**\n * Emit a chunk every N ms through `onChunk`, for streaming upload.\n *\n * Without it the whole recording is buffered in memory until `stop()` — fine for\n * a voice note, not fine for an hour-long meeting.\n */\n timesliceMs?: number;\n /** Receives each chunk when `timesliceMs` is set. Chunks are **not** independently playable. */\n onChunk?: (chunk: Blob) => void;\n /** Recorder-level failure (device unplugged mid-recording, encoder error). */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative recorder. */\nexport interface AudioRecorderHandle {\n /** Begin recording. No-op when already recording or paused. */\n start: () => void;\n /** Pause. The clock stops; `durationMs` freezes. */\n pause: () => void;\n /** Resume after `pause()`. */\n resume: () => void;\n /** Stop and resolve with the assembled recording. */\n stop: () => Promise<AudioRecording>;\n /** Stop and throw the audio away. */\n cancel: () => void;\n status: () => AudioRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: () => number;\n /** The negotiated container. */\n mimeType: string;\n}\n\n/** Whether `MediaRecorder` exists and can produce at least one audio container. */\nexport function isAudioRecordingSupported(): boolean {\n return typeof MediaRecorder !== \"undefined\" && pickAudioMimeType() !== null;\n}\n\n/**\n * First container in `preferred` the browser can actually produce, or `null`.\n *\n * **There is no MP3 or WAV here, and that is not an omission.** `MediaRecorder`\n * emits Opus (in WebM or Ogg) on Chromium and Firefox and AAC (in MP4) on Safari —\n * no engine implements an MP3 or WAV encoder for it. If a backend needs WAV,\n * {@link blobToWav} converts one client-side with no dependency; if it needs MP3,\n * transcode on the server. Shipping an MP3 encoder would mean a WASM build of the\n * order of 150 KB in every consumer's bundle to serve one format, which is the\n * trade this SDK does not make.\n *\n * @param preferred - Candidates, best first. Defaults to {@link AUDIO_MIME_CANDIDATES}.\n * @returns A supported MIME type, or `null` when none is.\n */\nexport function pickAudioMimeType(\n preferred: readonly string[] = AUDIO_MIME_CANDIDATES,\n): string | null {\n return pickRecordingMimeType(preferred);\n}\n\n/**\n * Wrap a `MediaStream` in a recorder with a real state machine and an honest clock.\n *\n * The engine is shared with the video recorder (`createMediaRecorder`), because the\n * subtle parts are not audio-specific: `MediaRecorder` reports **no duration** (WebM\n * it writes carries none in its header, which is why `<audio>` shows `Infinity` for a\n * fresh recording) so the clock is kept by hand and subtracts paused time; and\n * `stop()` returns *before* the last `dataavailable` event, so the blob can only be\n * assembled in `onstop`. What stays here is the part that genuinely differs — the\n * container list.\n *\n * The stream is **not** owned here: `stop()` leaves the microphone open so a retake\n * does not need a second permission round-trip. Release it with the owning\n * `useMicrophone().stop()`.\n *\n * @param stream - A live audio stream, usually from {@link useMicrophone}.\n * @param options - See {@link AudioRecorderOptions}.\n * @returns The imperative recorder.\n * @throws When `MediaRecorder` is unavailable, or an explicit `mimeType` is unsupported.\n */\nexport function createAudioRecorder(\n stream: MediaStream,\n options: AudioRecorderOptions = {},\n): AudioRecorderHandle {\n return createMediaRecorder(stream, {\n candidates: AUDIO_MIME_CANDIDATES,\n kind: \"audio\",\n mimeType: options.mimeType,\n audioBitsPerSecond: options.audioBitsPerSecond,\n timesliceMs: options.timesliceMs,\n onChunk: options.onChunk,\n onError: options.onError,\n });\n}\n"],"mappings":";;AAuBA,IAAa,IAA2C;CACpD;CACA;CACA;CACA;CACA;AACJ;AAwDA,SAAgB,IAAqC;CACjD,OAAO,OAAO,gBAAkB,OAAe,EAAkB,MAAM;AAC3E;AAgBA,SAAgB,EACZ,IAA+B,GAClB;CACb,OAAO,EAAsB,CAAS;AAC1C;AAsBA,SAAgB,EACZ,GACA,IAAgC,CAAC,GACd;CACnB,OAAO,EAAoB,GAAQ;EAC/B,YAAY;EACZ,MAAM;EACN,UAAU,EAAQ;EAClB,oBAAoB,EAAQ;EAC5B,aAAa,EAAQ;EACrB,SAAS,EAAQ;EACjB,SAAS,EAAQ;CACrB,CAAC;AACL"}
@@ -0,0 +1,2 @@
1
+ function e(e){if(!Number.isFinite(e)||e<0)return`--:--`;let t=Math.floor(e/1e3),n=t%60,r=Math.floor(t/60)%60,i=Math.floor(t/3600),a=e=>String(e).padStart(2,`0`);return i>0?`${i}:${a(r)}:${a(n)}`:`${r}:${a(n)}`}exports.formatDuration=e;
2
+ //# sourceMappingURL=duration.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duration.cjs","names":[],"sources":["../../src/audio/duration.ts"],"sourcesContent":["/**\n * Format a length in milliseconds as a clock: `0:07`, `2:41`, `1:03:12`.\n *\n * Returns `\"--:--\"` for anything not finite, which is the case that matters: a fresh\n * `MediaRecorder` WebM carries no duration in its header, so `<audio>.duration` reads\n * `Infinity` until the browser has been coaxed into resolving it. Rendering `NaN:aN`\n * there is the bug every home-made audio player ships with.\n *\n * The hour field appears only when there is one, so a voice note reads `0:07` rather\n * than `0:00:07`.\n *\n * @param ms - Length in milliseconds, or a non-finite value for \"unknown\".\n * @returns A clock string.\n */\nexport function formatDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return \"--:--\";\n const total = Math.floor(ms / 1000);\n const seconds = total % 60;\n const minutes = Math.floor(total / 60) % 60;\n const hours = Math.floor(total / 3600);\n const pad = (value: number): string => String(value).padStart(2, \"0\");\n return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${minutes}:${pad(seconds)}`;\n}\n"],"mappings":"AAcA,SAAgB,EAAe,EAAoB,CAC/C,GAAI,CAAC,OAAO,SAAS,CAAE,GAAK,EAAK,EAAG,MAAO,QAC3C,IAAM,EAAQ,KAAK,MAAM,EAAK,GAAI,EAC5B,EAAU,EAAQ,GAClB,EAAU,KAAK,MAAM,EAAQ,EAAE,EAAI,GACnC,EAAQ,KAAK,MAAM,EAAQ,IAAI,EAC/B,EAAO,GAA0B,OAAO,CAAK,CAAC,CAAC,SAAS,EAAG,GAAG,EACpE,OAAO,EAAQ,EAAI,GAAG,EAAM,GAAG,EAAI,CAAO,EAAE,GAAG,EAAI,CAAO,IAAM,GAAG,EAAQ,GAAG,EAAI,CAAO,GAC7F"}
@@ -0,0 +1,10 @@
1
+ //#region src/audio/duration.ts
2
+ function e(e) {
3
+ if (!Number.isFinite(e) || e < 0) return "--:--";
4
+ let t = Math.floor(e / 1e3), n = t % 60, r = Math.floor(t / 60) % 60, i = Math.floor(t / 3600), a = (e) => String(e).padStart(2, "0");
5
+ return i > 0 ? `${i}:${a(r)}:${a(n)}` : `${r}:${a(n)}`;
6
+ }
7
+ //#endregion
8
+ export { e as formatDuration };
9
+
10
+ //# sourceMappingURL=duration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duration.js","names":[],"sources":["../../src/audio/duration.ts"],"sourcesContent":["/**\n * Format a length in milliseconds as a clock: `0:07`, `2:41`, `1:03:12`.\n *\n * Returns `\"--:--\"` for anything not finite, which is the case that matters: a fresh\n * `MediaRecorder` WebM carries no duration in its header, so `<audio>.duration` reads\n * `Infinity` until the browser has been coaxed into resolving it. Rendering `NaN:aN`\n * there is the bug every home-made audio player ships with.\n *\n * The hour field appears only when there is one, so a voice note reads `0:07` rather\n * than `0:00:07`.\n *\n * @param ms - Length in milliseconds, or a non-finite value for \"unknown\".\n * @returns A clock string.\n */\nexport function formatDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return \"--:--\";\n const total = Math.floor(ms / 1000);\n const seconds = total % 60;\n const minutes = Math.floor(total / 60) % 60;\n const hours = Math.floor(total / 3600);\n const pad = (value: number): string => String(value).padStart(2, \"0\");\n return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${minutes}:${pad(seconds)}`;\n}\n"],"mappings":";AAcA,SAAgB,EAAe,GAAoB;CAC/C,IAAI,CAAC,OAAO,SAAS,CAAE,KAAK,IAAK,GAAG,OAAO;CAC3C,IAAM,IAAQ,KAAK,MAAM,IAAK,GAAI,GAC5B,IAAU,IAAQ,IAClB,IAAU,KAAK,MAAM,IAAQ,EAAE,IAAI,IACnC,IAAQ,KAAK,MAAM,IAAQ,IAAI,GAC/B,KAAO,MAA0B,OAAO,CAAK,CAAC,CAAC,SAAS,GAAG,GAAG;CACpE,OAAO,IAAQ,IAAI,GAAG,EAAM,GAAG,EAAI,CAAO,EAAE,GAAG,EAAI,CAAO,MAAM,GAAG,EAAQ,GAAG,EAAI,CAAO;AAC7F"}
@@ -0,0 +1,2 @@
1
+ function e(e,{fftSize:t=1024,decay:n=.7}={}){let r=typeof AudioContext<`u`?AudioContext:globalThis.webkitAudioContext;if(!r)return{level:()=>0,stop:()=>void 0};let i=new r,a=i.createMediaStreamSource(e),o=i.createAnalyser();o.fftSize=t,a.connect(o);let s=new Float32Array(o.fftSize),c=0,l=!1;return{level:()=>{if(l)return 0;o.getFloatTimeDomainData(s);let e=0;for(let t=0;t<s.length;t+=1)e+=s[t]*s[t];let t=Math.sqrt(e/s.length);return c=t>c?t:c*n+t*(1-n),Math.min(1,c)},stop:()=>{if(!l){l=!0;try{a.disconnect(),o.disconnect()}catch{}i.close().catch(()=>void 0)}}}}exports.createLevelMeter=e;
2
+ //# sourceMappingURL=level-meter.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"level-meter.cjs","names":[],"sources":["../../src/audio/level-meter.ts"],"sourcesContent":["/** Options for {@link createLevelMeter}. */\nexport interface LevelMeterOptions {\n /**\n * FFT window. 1024 is a good default: ~21 ms at 48 kHz, long enough to be stable\n * and short enough to react to a syllable.\n */\n fftSize?: number;\n /**\n * Extra smoothing on the way down, 0–1. Default `0.7`.\n *\n * A raw RMS reading is jittery enough to make a meter look broken. Attack is left\n * instant — a meter that lags the voice going *up* reads as \"not recording\" —\n * while the decay is eased, which is how every hardware meter behaves.\n */\n decay?: number;\n}\n\n/** A running level meter. */\nexport interface LevelMeter {\n /** Latest level, 0–1. Read it on your own frame loop. */\n level: () => number;\n /** Stop sampling and release the Web Audio graph. */\n stop: () => void;\n}\n\n/**\n * Sample the loudness of a live stream, 0–1.\n *\n * This exists because a recorder with no visible level is indistinguishable from a\n * broken one. A muted OS input, a headset whose mic arm is switched off, the wrong\n * device selected — all three produce a perfectly successful recording of silence,\n * and the user only finds out after they finish talking.\n *\n * The value is RMS, not peak: peak reacts to a single sample and flickers, RMS tracks\n * perceived loudness. It is deliberately **not** React state — updating state per\n * frame would re-render the tree 60 times a second. Poll `level()` from a\n * `requestAnimationFrame` loop and write to the DOM, or read it inside an existing\n * animation.\n *\n * The `AudioContext` is created here and closed by `stop()`. Browsers cap the number\n * of live contexts (Chrome allows around six), so a meter left running on unmount\n * eventually breaks every later one on the page.\n *\n * @param stream - A live audio stream.\n * @param options - See {@link LevelMeterOptions}.\n * @returns A `level()` reader and a `stop()`.\n *\n * @example\n * const meter = createLevelMeter(stream);\n * const tick = () => { bar.style.transform = `scaleX(${meter.level()})`; raf = requestAnimationFrame(tick); };\n */\nexport function createLevelMeter(\n stream: MediaStream,\n { fftSize = 1024, decay = 0.7 }: LevelMeterOptions = {},\n): LevelMeter {\n const Ctor: typeof AudioContext | undefined =\n typeof AudioContext !== \"undefined\"\n ? AudioContext\n : (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n\n if (!Ctor) {\n return { level: () => 0, stop: () => undefined };\n }\n\n const context = new Ctor();\n const source = context.createMediaStreamSource(stream);\n const analyser = context.createAnalyser();\n analyser.fftSize = fftSize;\n source.connect(analyser);\n\n const samples = new Float32Array(analyser.fftSize);\n let smoothed = 0;\n let closed = false;\n\n const read = (): number => {\n if (closed) return 0;\n analyser.getFloatTimeDomainData(samples);\n let sum = 0;\n for (let index = 0; index < samples.length; index += 1) {\n sum += samples[index] * samples[index];\n }\n const rms = Math.sqrt(sum / samples.length);\n // Instant attack, eased release.\n smoothed = rms > smoothed ? rms : smoothed * decay + rms * (1 - decay);\n return Math.min(1, smoothed);\n };\n\n return {\n level: read,\n stop: (): void => {\n if (closed) return;\n closed = true;\n try {\n source.disconnect();\n analyser.disconnect();\n } catch {\n // Already torn down with the context — nothing to release.\n }\n void context.close().catch(() => undefined);\n },\n };\n}\n"],"mappings":"AAmDA,SAAgB,EACZ,EACA,CAAE,UAAU,KAAM,QAAQ,IAA2B,CAAC,EAC5C,CACV,IAAM,EACF,OAAO,aAAiB,IAClB,aACC,WAA4D,mBAEvE,GAAI,CAAC,EACD,MAAO,CAAE,UAAa,EAAG,SAAY,IAAA,EAAU,EAGnD,IAAM,EAAU,IAAI,EACd,EAAS,EAAQ,wBAAwB,CAAM,EAC/C,EAAW,EAAQ,eAAe,EACxC,EAAS,QAAU,EACnB,EAAO,QAAQ,CAAQ,EAEvB,IAAM,EAAU,IAAI,aAAa,EAAS,OAAO,EAC7C,EAAW,EACX,EAAS,GAeb,MAAO,CACH,UAduB,CACvB,GAAI,EAAQ,MAAO,GACnB,EAAS,uBAAuB,CAAO,EACvC,IAAI,EAAM,EACV,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAQ,OAAQ,GAAS,EACjD,GAAO,EAAQ,GAAS,EAAQ,GAEpC,IAAM,EAAM,KAAK,KAAK,EAAM,EAAQ,MAAM,EAG1C,MADA,GAAW,EAAM,EAAW,EAAM,EAAW,EAAQ,GAAO,EAAI,GACzD,KAAK,IAAI,EAAG,CAAQ,CAC/B,EAII,SAAkB,CACV,MACJ,GAAS,GACT,GAAI,CACA,EAAO,WAAW,EAClB,EAAS,WAAW,CACxB,MAAQ,CAER,CACA,EAAa,MAAM,CAAC,CAAC,UAAY,IAAA,EAAS,CAPjC,CAQb,CACJ,CACJ"}
@@ -0,0 +1,34 @@
1
+ //#region src/audio/level-meter.ts
2
+ function e(e, { fftSize: t = 1024, decay: n = .7 } = {}) {
3
+ let r = typeof AudioContext < "u" ? AudioContext : globalThis.webkitAudioContext;
4
+ if (!r) return {
5
+ level: () => 0,
6
+ stop: () => void 0
7
+ };
8
+ let i = new r(), a = i.createMediaStreamSource(e), o = i.createAnalyser();
9
+ o.fftSize = t, a.connect(o);
10
+ let s = new Float32Array(o.fftSize), c = 0, l = !1;
11
+ return {
12
+ level: () => {
13
+ if (l) return 0;
14
+ o.getFloatTimeDomainData(s);
15
+ let e = 0;
16
+ for (let t = 0; t < s.length; t += 1) e += s[t] * s[t];
17
+ let t = Math.sqrt(e / s.length);
18
+ return c = t > c ? t : c * n + t * (1 - n), Math.min(1, c);
19
+ },
20
+ stop: () => {
21
+ if (!l) {
22
+ l = !0;
23
+ try {
24
+ a.disconnect(), o.disconnect();
25
+ } catch {}
26
+ i.close().catch(() => void 0);
27
+ }
28
+ }
29
+ };
30
+ }
31
+ //#endregion
32
+ export { e as createLevelMeter };
33
+
34
+ //# sourceMappingURL=level-meter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"level-meter.js","names":[],"sources":["../../src/audio/level-meter.ts"],"sourcesContent":["/** Options for {@link createLevelMeter}. */\nexport interface LevelMeterOptions {\n /**\n * FFT window. 1024 is a good default: ~21 ms at 48 kHz, long enough to be stable\n * and short enough to react to a syllable.\n */\n fftSize?: number;\n /**\n * Extra smoothing on the way down, 0–1. Default `0.7`.\n *\n * A raw RMS reading is jittery enough to make a meter look broken. Attack is left\n * instant — a meter that lags the voice going *up* reads as \"not recording\" —\n * while the decay is eased, which is how every hardware meter behaves.\n */\n decay?: number;\n}\n\n/** A running level meter. */\nexport interface LevelMeter {\n /** Latest level, 0–1. Read it on your own frame loop. */\n level: () => number;\n /** Stop sampling and release the Web Audio graph. */\n stop: () => void;\n}\n\n/**\n * Sample the loudness of a live stream, 0–1.\n *\n * This exists because a recorder with no visible level is indistinguishable from a\n * broken one. A muted OS input, a headset whose mic arm is switched off, the wrong\n * device selected — all three produce a perfectly successful recording of silence,\n * and the user only finds out after they finish talking.\n *\n * The value is RMS, not peak: peak reacts to a single sample and flickers, RMS tracks\n * perceived loudness. It is deliberately **not** React state — updating state per\n * frame would re-render the tree 60 times a second. Poll `level()` from a\n * `requestAnimationFrame` loop and write to the DOM, or read it inside an existing\n * animation.\n *\n * The `AudioContext` is created here and closed by `stop()`. Browsers cap the number\n * of live contexts (Chrome allows around six), so a meter left running on unmount\n * eventually breaks every later one on the page.\n *\n * @param stream - A live audio stream.\n * @param options - See {@link LevelMeterOptions}.\n * @returns A `level()` reader and a `stop()`.\n *\n * @example\n * const meter = createLevelMeter(stream);\n * const tick = () => { bar.style.transform = `scaleX(${meter.level()})`; raf = requestAnimationFrame(tick); };\n */\nexport function createLevelMeter(\n stream: MediaStream,\n { fftSize = 1024, decay = 0.7 }: LevelMeterOptions = {},\n): LevelMeter {\n const Ctor: typeof AudioContext | undefined =\n typeof AudioContext !== \"undefined\"\n ? AudioContext\n : (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n\n if (!Ctor) {\n return { level: () => 0, stop: () => undefined };\n }\n\n const context = new Ctor();\n const source = context.createMediaStreamSource(stream);\n const analyser = context.createAnalyser();\n analyser.fftSize = fftSize;\n source.connect(analyser);\n\n const samples = new Float32Array(analyser.fftSize);\n let smoothed = 0;\n let closed = false;\n\n const read = (): number => {\n if (closed) return 0;\n analyser.getFloatTimeDomainData(samples);\n let sum = 0;\n for (let index = 0; index < samples.length; index += 1) {\n sum += samples[index] * samples[index];\n }\n const rms = Math.sqrt(sum / samples.length);\n // Instant attack, eased release.\n smoothed = rms > smoothed ? rms : smoothed * decay + rms * (1 - decay);\n return Math.min(1, smoothed);\n };\n\n return {\n level: read,\n stop: (): void => {\n if (closed) return;\n closed = true;\n try {\n source.disconnect();\n analyser.disconnect();\n } catch {\n // Already torn down with the context — nothing to release.\n }\n void context.close().catch(() => undefined);\n },\n };\n}\n"],"mappings":";AAmDA,SAAgB,EACZ,GACA,EAAE,aAAU,MAAM,WAAQ,OAA2B,CAAC,GAC5C;CACV,IAAM,IACF,OAAO,eAAiB,MAClB,eACC,WAA4D;CAEvE,IAAI,CAAC,GACD,OAAO;EAAE,aAAa;EAAG,YAAY,KAAA;CAAU;CAGnD,IAAM,IAAU,IAAI,EAAK,GACnB,IAAS,EAAQ,wBAAwB,CAAM,GAC/C,IAAW,EAAQ,eAAe;CAExC,AADA,EAAS,UAAU,GACnB,EAAO,QAAQ,CAAQ;CAEvB,IAAM,IAAU,IAAI,aAAa,EAAS,OAAO,GAC7C,IAAW,GACX,IAAS;CAeb,OAAO;EACH,aAduB;GACvB,IAAI,GAAQ,OAAO;GACnB,EAAS,uBAAuB,CAAO;GACvC,IAAI,IAAM;GACV,KAAK,IAAI,IAAQ,GAAG,IAAQ,EAAQ,QAAQ,KAAS,GACjD,KAAO,EAAQ,KAAS,EAAQ;GAEpC,IAAM,IAAM,KAAK,KAAK,IAAM,EAAQ,MAAM;GAG1C,OADA,IAAW,IAAM,IAAW,IAAM,IAAW,IAAQ,KAAO,IAAI,IACzD,KAAK,IAAI,GAAG,CAAQ;EAC/B;EAII,YAAkB;GACV,QACJ;QAAS;IACT,IAAI;KAEA,AADA,EAAO,WAAW,GAClB,EAAS,WAAW;IACxB,QAAQ,CAER;IACA,EAAa,MAAM,CAAC,CAAC,YAAY,KAAA,CAAS;GAPjC;EAQb;CACJ;AACJ"}
@@ -0,0 +1,2 @@
1
+ var e={microphone:`Microphone`,camera:`Camera`,screen:`Screen`};function t(t,n=`microphone`){let r=e[n];if(typeof window>`u`)return{kind:`unsupported`,message:`${r} is unavailable in this environment.`};if(!window.isSecureContext)return{kind:`insecure`,message:`${r} access requires a secure (HTTPS) connection.`};if(t instanceof DOMException)switch(t.name){case`NotAllowedError`:case`SecurityError`:return{kind:`permission-denied`,message:`${r} permission denied. Enable access in your browser settings.`};case`NotFoundError`:case`OverconstrainedError`:return{kind:`not-found`,message:`No ${n} available on this device.`};case`NotReadableError`:case`AbortError`:return{kind:`in-use`,message:`The ${n} is in use by another app. Close it and try again.`}}return{kind:`unknown`,message:t instanceof Error?t.message:`Unexpected error while accessing the ${n}.`}}function n(){return typeof navigator<`u`&&navigator.mediaDevices!==void 0&&typeof navigator.mediaDevices.getUserMedia==`function`}function r(n=`microphone`){return typeof window<`u`&&!window.isSecureContext?t(null,n):{kind:`unsupported`,message:`${e[n]} capture is not supported in this browser.`}}exports.classifyMediaError=t,exports.isMediaCaptureSupported=n,exports.missingCaptureApiError=r;
2
+ //# sourceMappingURL=media-access.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-access.cjs","names":[],"sources":["../../src/audio/media-access.ts"],"sourcesContent":["/**\n * What a media error is about.\n *\n * `\"screen\"` is not a device in the `getUserMedia` sense — it is what\n * `getDisplayMedia` captures — but it fails through the same `DOMException` names, so\n * it is classified by the same function rather than by a second copy of the table.\n */\nexport type MediaDeviceKindLabel = \"microphone\" | \"camera\" | \"screen\";\n\n/** Classified reason a capture device could not be acquired. */\nexport type MediaAccessErrorKind =\n \"unsupported\" | \"insecure\" | \"permission-denied\" | \"not-found\" | \"in-use\" | \"unknown\";\n\n/** A classified capture error with a human-readable, English message. */\nexport interface MediaAccessError {\n kind: MediaAccessErrorKind;\n message: string;\n}\n\n/** Sentence fragment naming the device, so one classifier serves both. */\nconst NOUN: Record<MediaDeviceKindLabel, string> = {\n microphone: \"Microphone\",\n camera: \"Camera\",\n screen: \"Screen\",\n};\n\n/**\n * Map an unknown `getUserMedia` failure into a {@link MediaAccessError}.\n *\n * The order matters. A missing `navigator.mediaDevices` is almost never \"this\n * browser cannot do audio\" — it is a page served over plain HTTP, where the whole\n * API is simply absent. Reporting `unsupported` there sends the developer looking\n * for a polyfill for a problem an `https://` URL fixes, so the secure-context check\n * runs first and wins.\n *\n * The `DOMException.name` values are then collapsed into kinds an app can actually\n * branch on, because the raw names do not group the way a UI needs them:\n * `NotFoundError` and `OverconstrainedError` both mean \"you will not get a device\n * with these constraints\", and `NotReadableError` and `AbortError` both mean \"the\n * hardware is busy\" — most often another tab of the same app.\n *\n * @param err - Whatever `getUserMedia` (or a device query) rejected with.\n * @param device - Which device was being opened, for the message.\n * @returns A stable kind plus a message safe to show a user.\n */\nexport function classifyMediaError(\n err: unknown,\n device: MediaDeviceKindLabel = \"microphone\",\n): MediaAccessError {\n const noun = NOUN[device];\n\n if (typeof window === \"undefined\") {\n return { kind: \"unsupported\", message: `${noun} is unavailable in this environment.` };\n }\n if (!window.isSecureContext) {\n return {\n kind: \"insecure\",\n message: `${noun} access requires a secure (HTTPS) connection.`,\n };\n }\n if (err instanceof DOMException) {\n switch (err.name) {\n case \"NotAllowedError\":\n case \"SecurityError\":\n return {\n kind: \"permission-denied\",\n message: `${noun} permission denied. Enable access in your browser settings.`,\n };\n case \"NotFoundError\":\n case \"OverconstrainedError\":\n return {\n kind: \"not-found\",\n message: `No ${device} available on this device.`,\n };\n case \"NotReadableError\":\n case \"AbortError\":\n return {\n kind: \"in-use\",\n message: `The ${device} is in use by another app. Close it and try again.`,\n };\n }\n }\n return {\n kind: \"unknown\",\n message:\n err instanceof Error ? err.message : `Unexpected error while accessing the ${device}.`,\n };\n}\n\n/**\n * Whether `getUserMedia` is reachable at all.\n *\n * Separate from the classifier because a page often wants to hide a \"record\" button\n * entirely rather than render it and fail on click.\n */\nexport function isMediaCaptureSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n navigator.mediaDevices !== undefined &&\n typeof navigator.mediaDevices.getUserMedia === \"function\"\n );\n}\n\n/**\n * The error for \"the API is not here at all\", which is a different question from\n * \"the API rejected\".\n *\n * `classifyMediaError` takes a thrown value; a missing `navigator.mediaDevices` never\n * threw one, and handing it `null` would come back `unknown` — the least actionable\n * kind there is. The two real causes are an insecure page (the fix is a URL) and an\n * engine that genuinely cannot capture (the fix is another browser), and telling them\n * apart is the whole point.\n *\n * @param device - Which device was being opened, for the message.\n */\nexport function missingCaptureApiError(\n device: MediaDeviceKindLabel = \"microphone\",\n): MediaAccessError {\n if (typeof window !== \"undefined\" && !window.isSecureContext) {\n return classifyMediaError(null, device);\n }\n return {\n kind: \"unsupported\",\n message: `${NOUN[device]} capture is not supported in this browser.`,\n };\n}\n"],"mappings":"AAoBA,IAAM,EAA6C,CAC/C,WAAY,aACZ,OAAQ,SACR,OAAQ,QACZ,EAqBA,SAAgB,EACZ,EACA,EAA+B,aACf,CAChB,IAAM,EAAO,EAAK,GAElB,GAAI,OAAO,OAAW,IAClB,MAAO,CAAE,KAAM,cAAe,QAAS,GAAG,EAAK,qCAAsC,EAEzF,GAAI,CAAC,OAAO,gBACR,MAAO,CACH,KAAM,WACN,QAAS,GAAG,EAAK,8CACrB,EAEJ,GAAI,aAAe,aACf,OAAQ,EAAI,KAAZ,CACI,IAAK,kBACL,IAAK,gBACD,MAAO,CACH,KAAM,oBACN,QAAS,GAAG,EAAK,4DACrB,EACJ,IAAK,gBACL,IAAK,uBACD,MAAO,CACH,KAAM,YACN,QAAS,MAAM,EAAO,2BAC1B,EACJ,IAAK,mBACL,IAAK,aACD,MAAO,CACH,KAAM,SACN,QAAS,OAAO,EAAO,mDAC3B,CACR,CAEJ,MAAO,CACH,KAAM,UACN,QACI,aAAe,MAAQ,EAAI,QAAU,wCAAwC,EAAO,EAC5F,CACJ,CAQA,SAAgB,GAAmC,CAC/C,OACI,OAAO,UAAc,KACrB,UAAU,eAAiB,IAAA,IAC3B,OAAO,UAAU,aAAa,cAAiB,UAEvD,CAcA,SAAgB,EACZ,EAA+B,aACf,CAIhB,OAHI,OAAO,OAAW,KAAe,CAAC,OAAO,gBAClC,EAAmB,KAAM,CAAM,EAEnC,CACH,KAAM,cACN,QAAS,GAAG,EAAK,GAAQ,2CAC7B,CACJ"}
@@ -0,0 +1,51 @@
1
+ //#region src/audio/media-access.ts
2
+ var e = {
3
+ microphone: "Microphone",
4
+ camera: "Camera",
5
+ screen: "Screen"
6
+ };
7
+ function t(t, n = "microphone") {
8
+ let r = e[n];
9
+ if (typeof window > "u") return {
10
+ kind: "unsupported",
11
+ message: `${r} is unavailable in this environment.`
12
+ };
13
+ if (!window.isSecureContext) return {
14
+ kind: "insecure",
15
+ message: `${r} access requires a secure (HTTPS) connection.`
16
+ };
17
+ if (t instanceof DOMException) switch (t.name) {
18
+ case "NotAllowedError":
19
+ case "SecurityError": return {
20
+ kind: "permission-denied",
21
+ message: `${r} permission denied. Enable access in your browser settings.`
22
+ };
23
+ case "NotFoundError":
24
+ case "OverconstrainedError": return {
25
+ kind: "not-found",
26
+ message: `No ${n} available on this device.`
27
+ };
28
+ case "NotReadableError":
29
+ case "AbortError": return {
30
+ kind: "in-use",
31
+ message: `The ${n} is in use by another app. Close it and try again.`
32
+ };
33
+ }
34
+ return {
35
+ kind: "unknown",
36
+ message: t instanceof Error ? t.message : `Unexpected error while accessing the ${n}.`
37
+ };
38
+ }
39
+ function n() {
40
+ return typeof navigator < "u" && navigator.mediaDevices !== void 0 && typeof navigator.mediaDevices.getUserMedia == "function";
41
+ }
42
+ function r(n = "microphone") {
43
+ return typeof window < "u" && !window.isSecureContext ? t(null, n) : {
44
+ kind: "unsupported",
45
+ message: `${e[n]} capture is not supported in this browser.`
46
+ };
47
+ }
48
+ //#endregion
49
+ export { t as classifyMediaError, n as isMediaCaptureSupported, r as missingCaptureApiError };
50
+
51
+ //# sourceMappingURL=media-access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-access.js","names":[],"sources":["../../src/audio/media-access.ts"],"sourcesContent":["/**\n * What a media error is about.\n *\n * `\"screen\"` is not a device in the `getUserMedia` sense — it is what\n * `getDisplayMedia` captures — but it fails through the same `DOMException` names, so\n * it is classified by the same function rather than by a second copy of the table.\n */\nexport type MediaDeviceKindLabel = \"microphone\" | \"camera\" | \"screen\";\n\n/** Classified reason a capture device could not be acquired. */\nexport type MediaAccessErrorKind =\n \"unsupported\" | \"insecure\" | \"permission-denied\" | \"not-found\" | \"in-use\" | \"unknown\";\n\n/** A classified capture error with a human-readable, English message. */\nexport interface MediaAccessError {\n kind: MediaAccessErrorKind;\n message: string;\n}\n\n/** Sentence fragment naming the device, so one classifier serves both. */\nconst NOUN: Record<MediaDeviceKindLabel, string> = {\n microphone: \"Microphone\",\n camera: \"Camera\",\n screen: \"Screen\",\n};\n\n/**\n * Map an unknown `getUserMedia` failure into a {@link MediaAccessError}.\n *\n * The order matters. A missing `navigator.mediaDevices` is almost never \"this\n * browser cannot do audio\" — it is a page served over plain HTTP, where the whole\n * API is simply absent. Reporting `unsupported` there sends the developer looking\n * for a polyfill for a problem an `https://` URL fixes, so the secure-context check\n * runs first and wins.\n *\n * The `DOMException.name` values are then collapsed into kinds an app can actually\n * branch on, because the raw names do not group the way a UI needs them:\n * `NotFoundError` and `OverconstrainedError` both mean \"you will not get a device\n * with these constraints\", and `NotReadableError` and `AbortError` both mean \"the\n * hardware is busy\" — most often another tab of the same app.\n *\n * @param err - Whatever `getUserMedia` (or a device query) rejected with.\n * @param device - Which device was being opened, for the message.\n * @returns A stable kind plus a message safe to show a user.\n */\nexport function classifyMediaError(\n err: unknown,\n device: MediaDeviceKindLabel = \"microphone\",\n): MediaAccessError {\n const noun = NOUN[device];\n\n if (typeof window === \"undefined\") {\n return { kind: \"unsupported\", message: `${noun} is unavailable in this environment.` };\n }\n if (!window.isSecureContext) {\n return {\n kind: \"insecure\",\n message: `${noun} access requires a secure (HTTPS) connection.`,\n };\n }\n if (err instanceof DOMException) {\n switch (err.name) {\n case \"NotAllowedError\":\n case \"SecurityError\":\n return {\n kind: \"permission-denied\",\n message: `${noun} permission denied. Enable access in your browser settings.`,\n };\n case \"NotFoundError\":\n case \"OverconstrainedError\":\n return {\n kind: \"not-found\",\n message: `No ${device} available on this device.`,\n };\n case \"NotReadableError\":\n case \"AbortError\":\n return {\n kind: \"in-use\",\n message: `The ${device} is in use by another app. Close it and try again.`,\n };\n }\n }\n return {\n kind: \"unknown\",\n message:\n err instanceof Error ? err.message : `Unexpected error while accessing the ${device}.`,\n };\n}\n\n/**\n * Whether `getUserMedia` is reachable at all.\n *\n * Separate from the classifier because a page often wants to hide a \"record\" button\n * entirely rather than render it and fail on click.\n */\nexport function isMediaCaptureSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n navigator.mediaDevices !== undefined &&\n typeof navigator.mediaDevices.getUserMedia === \"function\"\n );\n}\n\n/**\n * The error for \"the API is not here at all\", which is a different question from\n * \"the API rejected\".\n *\n * `classifyMediaError` takes a thrown value; a missing `navigator.mediaDevices` never\n * threw one, and handing it `null` would come back `unknown` — the least actionable\n * kind there is. The two real causes are an insecure page (the fix is a URL) and an\n * engine that genuinely cannot capture (the fix is another browser), and telling them\n * apart is the whole point.\n *\n * @param device - Which device was being opened, for the message.\n */\nexport function missingCaptureApiError(\n device: MediaDeviceKindLabel = \"microphone\",\n): MediaAccessError {\n if (typeof window !== \"undefined\" && !window.isSecureContext) {\n return classifyMediaError(null, device);\n }\n return {\n kind: \"unsupported\",\n message: `${NOUN[device]} capture is not supported in this browser.`,\n };\n}\n"],"mappings":";AAoBA,IAAM,IAA6C;CAC/C,YAAY;CACZ,QAAQ;CACR,QAAQ;AACZ;AAqBA,SAAgB,EACZ,GACA,IAA+B,cACf;CAChB,IAAM,IAAO,EAAK;CAElB,IAAI,OAAO,SAAW,KAClB,OAAO;EAAE,MAAM;EAAe,SAAS,GAAG,EAAK;CAAsC;CAEzF,IAAI,CAAC,OAAO,iBACR,OAAO;EACH,MAAM;EACN,SAAS,GAAG,EAAK;CACrB;CAEJ,IAAI,aAAe,cACf,QAAQ,EAAI,MAAZ;EACI,KAAK;EACL,KAAK,iBACD,OAAO;GACH,MAAM;GACN,SAAS,GAAG,EAAK;EACrB;EACJ,KAAK;EACL,KAAK,wBACD,OAAO;GACH,MAAM;GACN,SAAS,MAAM,EAAO;EAC1B;EACJ,KAAK;EACL,KAAK,cACD,OAAO;GACH,MAAM;GACN,SAAS,OAAO,EAAO;EAC3B;CACR;CAEJ,OAAO;EACH,MAAM;EACN,SACI,aAAe,QAAQ,EAAI,UAAU,wCAAwC,EAAO;CAC5F;AACJ;AAQA,SAAgB,IAAmC;CAC/C,OACI,OAAO,YAAc,OACrB,UAAU,iBAAiB,KAAA,KAC3B,OAAO,UAAU,aAAa,gBAAiB;AAEvD;AAcA,SAAgB,EACZ,IAA+B,cACf;CAIhB,OAHI,OAAO,SAAW,OAAe,CAAC,OAAO,kBAClC,EAAmB,MAAM,CAAM,IAEnC;EACH,MAAM;EACN,SAAS,GAAG,EAAK,GAAQ;CAC7B;AACJ"}
@@ -0,0 +1,2 @@
1
+ const e=require("../hooks/use-stable-callback.cjs"),t=require("./audio-recorder.cjs"),n=require("./level-meter.cjs");let r=require("react");function i(i,a={}){let{maxDurationMs:o,onRecorded:s,onChunk:c,onError:l,mimeType:u,audioBitsPerSecond:d,timesliceMs:f,tickMs:p=100,disableLevelMeter:m=!1}=a,[h,g]=(0,r.useState)(`idle`),[_,v]=(0,r.useState)(0),[y,b]=(0,r.useState)(0),[x,S]=(0,r.useState)(null),[C,w]=(0,r.useState)(null),[T,E]=(0,r.useState)(!1),D=(0,r.useRef)(null),O=(0,r.useRef)(null),k=e.useStableCallback(e=>s?.(e)),A=e.useStableCallback(e=>c?.(e)),j=e.useStableCallback(e=>{w(e),l?.(e)});(0,r.useEffect)(()=>{if(!i){E(!1);return}let e;try{e=t.createAudioRecorder(i,{mimeType:u,audioBitsPerSecond:d,timesliceMs:f,onChunk:A,onError:j})}catch(e){w(e),E(!1);return}return D.current=e,O.current=m?null:n.createLevelMeter(i),E(!0),g(`idle`),()=>{(e.status()===`recording`||e.status()===`paused`)&&e.cancel(),O.current?.stop(),O.current=null,D.current=null,E(!1)}},[i,m,u,d,f,A,j]);let M=(0,r.useCallback)(async()=>{let e=D.current;if(!e)return null;let t=e.status();if(t!==`recording`&&t!==`paused`)return null;let n=await e.stop();return g(`stopped`),v(n.durationMs),b(0),S(n),k(n),n},[k]);return(0,r.useEffect)(()=>{if(h!==`recording`&&h!==`paused`){b(0);return}let e=setInterval(()=>{let e=D.current;if(!e)return;let t=e.durationMs();v(t),b(h===`recording`?O.current?.level()??0:0),o!==void 0&&t>=o&&M()},p);return()=>clearInterval(e)},[h,p,o,M]),{status:h,durationMs:_,level:y,recording:x,error:C,ready:T,start:(0,r.useCallback)(()=>{let e=D.current;e&&(S(null),w(null),v(0),e.start(),g(e.status()))},[]),pause:(0,r.useCallback)(()=>{let e=D.current;e&&(e.pause(),v(e.durationMs()),g(e.status()))},[]),resume:(0,r.useCallback)(()=>{let e=D.current;e&&(e.resume(),g(e.status()))},[]),stop:M,cancel:(0,r.useCallback)(()=>{let e=D.current;e&&(e.cancel(),g(`idle`),v(0),b(0),S(null))},[])}}exports.useAudioRecorder=i;
2
+ //# sourceMappingURL=use-audio-recorder.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-audio-recorder.cjs","names":[],"sources":["../../src/audio/use-audio-recorder.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\nimport {\n createAudioRecorder,\n type AudioRecorderHandle,\n type AudioRecorderOptions,\n type AudioRecorderStatus,\n type AudioRecording,\n} from \"./audio-recorder\";\nimport { createLevelMeter, type LevelMeter } from \"./level-meter\";\n\n/** Options for {@link useAudioRecorder}. */\nexport interface UseAudioRecorderOptions extends AudioRecorderOptions {\n /**\n * Stop automatically after this many milliseconds.\n *\n * Worth setting on anything user-facing: a recording left running by accident is\n * a memory buffer that grows until the tab dies.\n */\n maxDurationMs?: number;\n /** Called once the recording is assembled, whether by `stop()` or `maxDurationMs`. */\n onRecorded?: (recording: AudioRecording) => void;\n /** How often the clock and the level are published, in ms. Default 100. */\n tickMs?: number;\n /** Skip the level meter — it costs an `AudioContext`. Default `false`. */\n disableLevelMeter?: boolean;\n}\n\n/** Value returned by {@link useAudioRecorder}. */\nexport interface UseAudioRecorderResult {\n status: AudioRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: number;\n /** Loudness 0–1, or `0` when the meter is off or nothing is recording. */\n level: number;\n /** The finished recording, or `null` before the first `stop()`. */\n recording: AudioRecording | null;\n /** Recorder-level error (encoder failure, device pulled mid-recording). */\n error: unknown;\n /** `false` when there is no stream yet, or the browser cannot record. */\n ready: boolean;\n start: () => void;\n pause: () => void;\n resume: () => void;\n /** Stop and resolve with the recording. Also published on `recording`. */\n stop: () => Promise<AudioRecording | null>;\n /** Stop and throw the audio away. */\n cancel: () => void;\n}\n\n/**\n * Record the given stream, with a clock, a level meter and a state machine.\n *\n * Pass the stream from {@link useMicrophone}; the hook stays `ready: false` until\n * there is one, so a page can render the whole recorder UI before permission is\n * granted and simply have it disabled.\n *\n * The clock and the level are published on an interval (`tickMs`, default 100 ms)\n * rather than per animation frame. A hook that set state 60 times a second would\n * re-render everything the recorder is inside; 10 Hz is smooth enough for a timer and\n * a meter, and the underlying `createLevelMeter` is still available for anyone who\n * wants a frame-accurate bar written straight to the DOM.\n *\n * @param stream - The stream to record, or `null` while permission is pending.\n * @param options - See {@link UseAudioRecorderOptions}.\n *\n * @example\n * const mic = useMicrophone();\n * const rec = useAudioRecorder(mic.stream, { maxDurationMs: 120_000 });\n * <button disabled={!rec.ready} onClick={rec.start}>Gravar</button>\n */\nexport function useAudioRecorder(\n stream: MediaStream | null,\n options: UseAudioRecorderOptions = {},\n): UseAudioRecorderResult {\n const {\n maxDurationMs,\n onRecorded,\n onChunk,\n onError,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n tickMs = 100,\n disableLevelMeter = false,\n } = options;\n\n const [status, setStatus] = useState<AudioRecorderStatus>(\"idle\");\n const [durationMs, setDurationMs] = useState(0);\n const [level, setLevel] = useState(0);\n const [recording, setRecording] = useState<AudioRecording | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [ready, setReady] = useState(false);\n\n const recorderRef = useRef<AudioRecorderHandle | null>(null);\n const meterRef = useRef<LevelMeter | null>(null);\n\n const emitRecorded = useStableCallback((result: AudioRecording) => onRecorded?.(result));\n const emitChunk = useStableCallback((chunk: Blob) => onChunk?.(chunk));\n const emitError = useStableCallback((err: unknown) => {\n setError(err);\n onError?.(err);\n });\n\n /**\n * Build the recorder and the meter for a stream, and tear both down when it\n * changes.\n *\n * The recorder is rebuilt per stream because `MediaRecorder` is bound to the\n * stream it was constructed with — swapping microphones mid-session hands us a\n * new `MediaStream`, and reusing the old recorder would keep recording the device\n * the user just switched away from. It is rebuilt on a container or bitrate change\n * for the same reason: both are constructor arguments.\n */\n useEffect(() => {\n if (!stream) {\n setReady(false);\n return;\n }\n let recorder: AudioRecorderHandle;\n try {\n recorder = createAudioRecorder(stream, {\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n onChunk: emitChunk,\n onError: emitError,\n });\n } catch (err) {\n setError(err);\n setReady(false);\n return;\n }\n recorderRef.current = recorder;\n meterRef.current = disableLevelMeter ? null : createLevelMeter(stream);\n setReady(true);\n setStatus(\"idle\");\n\n return () => {\n if (recorder.status() === \"recording\" || recorder.status() === \"paused\") {\n recorder.cancel();\n }\n meterRef.current?.stop();\n meterRef.current = null;\n recorderRef.current = null;\n setReady(false);\n };\n }, [\n stream,\n disableLevelMeter,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n emitChunk,\n emitError,\n ]);\n\n const stop = useCallback(async (): Promise<AudioRecording | null> => {\n const recorder = recorderRef.current;\n if (!recorder) return null;\n const current = recorder.status();\n if (current !== \"recording\" && current !== \"paused\") return null;\n const result = await recorder.stop();\n setStatus(\"stopped\");\n setDurationMs(result.durationMs);\n setLevel(0);\n setRecording(result);\n emitRecorded(result);\n return result;\n }, [emitRecorded]);\n\n /**\n * Publish the clock and the level while something is running, and enforce\n * `maxDurationMs`.\n *\n * The cap is checked on the same tick rather than with a `setTimeout` armed at\n * `start()`, because a paused recording must not keep counting toward it — a\n * timeout would fire while the user is paused mid-sentence.\n */\n useEffect(() => {\n if (status !== \"recording\" && status !== \"paused\") {\n setLevel(0);\n return;\n }\n const id = setInterval(() => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n const elapsed = recorder.durationMs();\n setDurationMs(elapsed);\n setLevel(status === \"recording\" ? (meterRef.current?.level() ?? 0) : 0);\n if (maxDurationMs !== undefined && elapsed >= maxDurationMs) void stop();\n }, tickMs);\n return () => clearInterval(id);\n }, [status, tickMs, maxDurationMs, stop]);\n\n const start = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n setRecording(null);\n setError(null);\n setDurationMs(0);\n recorder.start();\n setStatus(recorder.status());\n }, []);\n\n const pause = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.pause();\n setDurationMs(recorder.durationMs());\n setStatus(recorder.status());\n }, []);\n\n const resume = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.resume();\n setStatus(recorder.status());\n }, []);\n\n const cancel = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.cancel();\n setStatus(\"idle\");\n setDurationMs(0);\n setLevel(0);\n setRecording(null);\n }, []);\n\n return {\n status,\n durationMs,\n level,\n recording,\n error,\n ready,\n start,\n pause,\n resume,\n stop,\n cancel,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ import { useStableCallback as e } from "../hooks/use-stable-callback.js";
2
+ import { createAudioRecorder as t } from "./audio-recorder.js";
3
+ import { createLevelMeter as n } from "./level-meter.js";
4
+ import { useCallback as r, useEffect as i, useRef as a, useState as o } from "react";
5
+ //#region src/audio/use-audio-recorder.ts
6
+ function s(s, c = {}) {
7
+ let { maxDurationMs: l, onRecorded: u, onChunk: d, onError: f, mimeType: p, audioBitsPerSecond: m, timesliceMs: h, tickMs: g = 100, disableLevelMeter: _ = !1 } = c, [v, y] = o("idle"), [b, x] = o(0), [S, C] = o(0), [w, T] = o(null), [E, D] = o(null), [O, k] = o(!1), A = a(null), j = a(null), M = e((e) => u?.(e)), N = e((e) => d?.(e)), P = e((e) => {
8
+ D(e), f?.(e);
9
+ });
10
+ i(() => {
11
+ if (!s) {
12
+ k(!1);
13
+ return;
14
+ }
15
+ let e;
16
+ try {
17
+ e = t(s, {
18
+ mimeType: p,
19
+ audioBitsPerSecond: m,
20
+ timesliceMs: h,
21
+ onChunk: N,
22
+ onError: P
23
+ });
24
+ } catch (e) {
25
+ D(e), k(!1);
26
+ return;
27
+ }
28
+ return A.current = e, j.current = _ ? null : n(s), k(!0), y("idle"), () => {
29
+ (e.status() === "recording" || e.status() === "paused") && e.cancel(), j.current?.stop(), j.current = null, A.current = null, k(!1);
30
+ };
31
+ }, [
32
+ s,
33
+ _,
34
+ p,
35
+ m,
36
+ h,
37
+ N,
38
+ P
39
+ ]);
40
+ let F = r(async () => {
41
+ let e = A.current;
42
+ if (!e) return null;
43
+ let t = e.status();
44
+ if (t !== "recording" && t !== "paused") return null;
45
+ let n = await e.stop();
46
+ return y("stopped"), x(n.durationMs), C(0), T(n), M(n), n;
47
+ }, [M]);
48
+ return i(() => {
49
+ if (v !== "recording" && v !== "paused") {
50
+ C(0);
51
+ return;
52
+ }
53
+ let e = setInterval(() => {
54
+ let e = A.current;
55
+ if (!e) return;
56
+ let t = e.durationMs();
57
+ x(t), C(v === "recording" ? j.current?.level() ?? 0 : 0), l !== void 0 && t >= l && F();
58
+ }, g);
59
+ return () => clearInterval(e);
60
+ }, [
61
+ v,
62
+ g,
63
+ l,
64
+ F
65
+ ]), {
66
+ status: v,
67
+ durationMs: b,
68
+ level: S,
69
+ recording: w,
70
+ error: E,
71
+ ready: O,
72
+ start: r(() => {
73
+ let e = A.current;
74
+ e && (T(null), D(null), x(0), e.start(), y(e.status()));
75
+ }, []),
76
+ pause: r(() => {
77
+ let e = A.current;
78
+ e && (e.pause(), x(e.durationMs()), y(e.status()));
79
+ }, []),
80
+ resume: r(() => {
81
+ let e = A.current;
82
+ e && (e.resume(), y(e.status()));
83
+ }, []),
84
+ stop: F,
85
+ cancel: r(() => {
86
+ let e = A.current;
87
+ e && (e.cancel(), y("idle"), x(0), C(0), T(null));
88
+ }, [])
89
+ };
90
+ }
91
+ //#endregion
92
+ export { s as useAudioRecorder };
93
+
94
+ //# sourceMappingURL=use-audio-recorder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-audio-recorder.js","names":[],"sources":["../../src/audio/use-audio-recorder.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\nimport {\n createAudioRecorder,\n type AudioRecorderHandle,\n type AudioRecorderOptions,\n type AudioRecorderStatus,\n type AudioRecording,\n} from \"./audio-recorder\";\nimport { createLevelMeter, type LevelMeter } from \"./level-meter\";\n\n/** Options for {@link useAudioRecorder}. */\nexport interface UseAudioRecorderOptions extends AudioRecorderOptions {\n /**\n * Stop automatically after this many milliseconds.\n *\n * Worth setting on anything user-facing: a recording left running by accident is\n * a memory buffer that grows until the tab dies.\n */\n maxDurationMs?: number;\n /** Called once the recording is assembled, whether by `stop()` or `maxDurationMs`. */\n onRecorded?: (recording: AudioRecording) => void;\n /** How often the clock and the level are published, in ms. Default 100. */\n tickMs?: number;\n /** Skip the level meter — it costs an `AudioContext`. Default `false`. */\n disableLevelMeter?: boolean;\n}\n\n/** Value returned by {@link useAudioRecorder}. */\nexport interface UseAudioRecorderResult {\n status: AudioRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: number;\n /** Loudness 0–1, or `0` when the meter is off or nothing is recording. */\n level: number;\n /** The finished recording, or `null` before the first `stop()`. */\n recording: AudioRecording | null;\n /** Recorder-level error (encoder failure, device pulled mid-recording). */\n error: unknown;\n /** `false` when there is no stream yet, or the browser cannot record. */\n ready: boolean;\n start: () => void;\n pause: () => void;\n resume: () => void;\n /** Stop and resolve with the recording. Also published on `recording`. */\n stop: () => Promise<AudioRecording | null>;\n /** Stop and throw the audio away. */\n cancel: () => void;\n}\n\n/**\n * Record the given stream, with a clock, a level meter and a state machine.\n *\n * Pass the stream from {@link useMicrophone}; the hook stays `ready: false` until\n * there is one, so a page can render the whole recorder UI before permission is\n * granted and simply have it disabled.\n *\n * The clock and the level are published on an interval (`tickMs`, default 100 ms)\n * rather than per animation frame. A hook that set state 60 times a second would\n * re-render everything the recorder is inside; 10 Hz is smooth enough for a timer and\n * a meter, and the underlying `createLevelMeter` is still available for anyone who\n * wants a frame-accurate bar written straight to the DOM.\n *\n * @param stream - The stream to record, or `null` while permission is pending.\n * @param options - See {@link UseAudioRecorderOptions}.\n *\n * @example\n * const mic = useMicrophone();\n * const rec = useAudioRecorder(mic.stream, { maxDurationMs: 120_000 });\n * <button disabled={!rec.ready} onClick={rec.start}>Gravar</button>\n */\nexport function useAudioRecorder(\n stream: MediaStream | null,\n options: UseAudioRecorderOptions = {},\n): UseAudioRecorderResult {\n const {\n maxDurationMs,\n onRecorded,\n onChunk,\n onError,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n tickMs = 100,\n disableLevelMeter = false,\n } = options;\n\n const [status, setStatus] = useState<AudioRecorderStatus>(\"idle\");\n const [durationMs, setDurationMs] = useState(0);\n const [level, setLevel] = useState(0);\n const [recording, setRecording] = useState<AudioRecording | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [ready, setReady] = useState(false);\n\n const recorderRef = useRef<AudioRecorderHandle | null>(null);\n const meterRef = useRef<LevelMeter | null>(null);\n\n const emitRecorded = useStableCallback((result: AudioRecording) => onRecorded?.(result));\n const emitChunk = useStableCallback((chunk: Blob) => onChunk?.(chunk));\n const emitError = useStableCallback((err: unknown) => {\n setError(err);\n onError?.(err);\n });\n\n /**\n * Build the recorder and the meter for a stream, and tear both down when it\n * changes.\n *\n * The recorder is rebuilt per stream because `MediaRecorder` is bound to the\n * stream it was constructed with — swapping microphones mid-session hands us a\n * new `MediaStream`, and reusing the old recorder would keep recording the device\n * the user just switched away from. It is rebuilt on a container or bitrate change\n * for the same reason: both are constructor arguments.\n */\n useEffect(() => {\n if (!stream) {\n setReady(false);\n return;\n }\n let recorder: AudioRecorderHandle;\n try {\n recorder = createAudioRecorder(stream, {\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n onChunk: emitChunk,\n onError: emitError,\n });\n } catch (err) {\n setError(err);\n setReady(false);\n return;\n }\n recorderRef.current = recorder;\n meterRef.current = disableLevelMeter ? null : createLevelMeter(stream);\n setReady(true);\n setStatus(\"idle\");\n\n return () => {\n if (recorder.status() === \"recording\" || recorder.status() === \"paused\") {\n recorder.cancel();\n }\n meterRef.current?.stop();\n meterRef.current = null;\n recorderRef.current = null;\n setReady(false);\n };\n }, [\n stream,\n disableLevelMeter,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n emitChunk,\n emitError,\n ]);\n\n const stop = useCallback(async (): Promise<AudioRecording | null> => {\n const recorder = recorderRef.current;\n if (!recorder) return null;\n const current = recorder.status();\n if (current !== \"recording\" && current !== \"paused\") return null;\n const result = await recorder.stop();\n setStatus(\"stopped\");\n setDurationMs(result.durationMs);\n setLevel(0);\n setRecording(result);\n emitRecorded(result);\n return result;\n }, [emitRecorded]);\n\n /**\n * Publish the clock and the level while something is running, and enforce\n * `maxDurationMs`.\n *\n * The cap is checked on the same tick rather than with a `setTimeout` armed at\n * `start()`, because a paused recording must not keep counting toward it — a\n * timeout would fire while the user is paused mid-sentence.\n */\n useEffect(() => {\n if (status !== \"recording\" && status !== \"paused\") {\n setLevel(0);\n return;\n }\n const id = setInterval(() => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n const elapsed = recorder.durationMs();\n setDurationMs(elapsed);\n setLevel(status === \"recording\" ? (meterRef.current?.level() ?? 0) : 0);\n if (maxDurationMs !== undefined && elapsed >= maxDurationMs) void stop();\n }, tickMs);\n return () => clearInterval(id);\n }, [status, tickMs, maxDurationMs, stop]);\n\n const start = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n setRecording(null);\n setError(null);\n setDurationMs(0);\n recorder.start();\n setStatus(recorder.status());\n }, []);\n\n const pause = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.pause();\n setDurationMs(recorder.durationMs());\n setStatus(recorder.status());\n }, []);\n\n const resume = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.resume();\n setStatus(recorder.status());\n }, []);\n\n const cancel = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.cancel();\n setStatus(\"idle\");\n setDurationMs(0);\n setLevel(0);\n setRecording(null);\n }, []);\n\n return {\n status,\n durationMs,\n level,\n recording,\n error,\n ready,\n start,\n pause,\n resume,\n stop,\n cancel,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-audio.cjs","names":[],"sources":["../../src/audio/use-audio.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\nimport { createAudioPlayer, type AudioPlayer, type PlayAudioOptions } from \"./audio-player\";\n\nexport interface UseAudioResult {\n /** Play `src` on the hook's private player. */\n play: (src: string, options?: PlayAudioOptions) => Promise<void>;\n /** Stop the current clip. */\n stop: () => void;\n /**\n * Whether the browser's autoplay policy has been satisfied. Becomes\n * `true` after the first successful `play()`.\n */\n unlocked: boolean;\n}\n\n/**\n * The player lives in a state initializer, not in a lazily-written ref: the\n * initializer runs exactly once and is not a side effect during render, which is\n * what the ref version was. The player identity never changes, so state holds it\n * fine and the value is non-null from the first render.\n *\n * Hook-managed audio player. Each component instance gets its own\n * {@link AudioPlayer}, so unmounting cleanly stops playback. Useful for\n * notification chimes, UI feedback sounds, and per-component soundtracks.\n */\nexport function useAudio(): UseAudioResult {\n const [player] = useState<AudioPlayer>(() => createAudioPlayer());\n const [unlocked, setUnlocked] = useState<boolean>(false);\n\n useEffect(() => {\n return () => {\n player.stop();\n };\n }, [player]);\n\n const play = useCallback(\n async (src: string, options?: PlayAudioOptions): Promise<void> => {\n const result = await player.play(src, options);\n if (result) setUnlocked(true);\n },\n [player],\n );\n\n const stop = useCallback((): void => {\n player.stop();\n }, [player]);\n\n return { play, stop, unlocked };\n}\n"],"mappings":"6DAyBA,SAAgB,GAA2B,CACvC,GAAM,CAAC,IAAA,EAAA,EAAA,SAAA,KAAsC,EAAA,kBAAkB,CAAC,EAC1D,CAAC,EAAU,IAAA,EAAA,EAAA,SAAA,CAAiC,EAAK,EAoBvD,OAlBA,EAAA,EAAA,UAAA,SACiB,CACT,EAAO,KAAK,CAChB,EACD,CAAC,CAAM,CAAC,EAcJ,CAAE,MAAA,EAAA,EAAA,YAAA,CAXL,MAAO,EAAa,IAA8C,CAE1D,MADiB,EAAO,KAAK,EAAK,CAAO,GACjC,EAAY,EAAI,CAChC,EACA,CAAC,CAAM,CAOF,EAAM,MAAA,EAAA,EAAA,YAAA,KAJsB,CACjC,EAAO,KAAK,CAChB,EAAG,CAAC,CAAM,CAEK,EAAM,UAAS,CAClC"}
1
+ {"version":3,"file":"use-audio.cjs","names":[],"sources":["../../src/audio/use-audio.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\nimport { createAudioPlayer, type AudioPlayerHandle, type PlayAudioOptions } from \"./audio-player\";\n\nexport interface UseAudioResult {\n /** Play `src` on the hook's private player. */\n play: (src: string, options?: PlayAudioOptions) => Promise<void>;\n /** Stop the current clip. */\n stop: () => void;\n /**\n * Whether the browser's autoplay policy has been satisfied. Becomes\n * `true` after the first successful `play()`.\n */\n unlocked: boolean;\n}\n\n/**\n * The player lives in a state initializer, not in a lazily-written ref: the\n * initializer runs exactly once and is not a side effect during render, which is\n * what the ref version was. The player identity never changes, so state holds it\n * fine and the value is non-null from the first render.\n *\n * Hook-managed audio player. Each component instance gets its own\n * {@link AudioPlayerHandle}, so unmounting cleanly stops playback. Useful for\n * notification chimes, UI feedback sounds, and per-component soundtracks.\n */\nexport function useAudio(): UseAudioResult {\n const [player] = useState<AudioPlayerHandle>(() => createAudioPlayer());\n const [unlocked, setUnlocked] = useState<boolean>(false);\n\n useEffect(() => {\n return () => {\n player.stop();\n };\n }, [player]);\n\n const play = useCallback(\n async (src: string, options?: PlayAudioOptions): Promise<void> => {\n const result = await player.play(src, options);\n if (result) setUnlocked(true);\n },\n [player],\n );\n\n const stop = useCallback((): void => {\n player.stop();\n }, [player]);\n\n return { play, stop, unlocked };\n}\n"],"mappings":"6DAyBA,SAAgB,GAA2B,CACvC,GAAM,CAAC,IAAA,EAAA,EAAA,SAAA,KAA4C,EAAA,kBAAkB,CAAC,EAChE,CAAC,EAAU,IAAA,EAAA,EAAA,SAAA,CAAiC,EAAK,EAoBvD,OAlBA,EAAA,EAAA,UAAA,SACiB,CACT,EAAO,KAAK,CAChB,EACD,CAAC,CAAM,CAAC,EAcJ,CAAE,MAAA,EAAA,EAAA,YAAA,CAXL,MAAO,EAAa,IAA8C,CAE1D,MADiB,EAAO,KAAK,EAAK,CAAO,GACjC,EAAY,EAAI,CAChC,EACA,CAAC,CAAM,CAOF,EAAM,MAAA,EAAA,EAAA,YAAA,KAJsB,CACjC,EAAO,KAAK,CAChB,EAAG,CAAC,CAAM,CAEK,EAAM,UAAS,CAClC"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-audio.js","names":[],"sources":["../../src/audio/use-audio.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\nimport { createAudioPlayer, type AudioPlayer, type PlayAudioOptions } from \"./audio-player\";\n\nexport interface UseAudioResult {\n /** Play `src` on the hook's private player. */\n play: (src: string, options?: PlayAudioOptions) => Promise<void>;\n /** Stop the current clip. */\n stop: () => void;\n /**\n * Whether the browser's autoplay policy has been satisfied. Becomes\n * `true` after the first successful `play()`.\n */\n unlocked: boolean;\n}\n\n/**\n * The player lives in a state initializer, not in a lazily-written ref: the\n * initializer runs exactly once and is not a side effect during render, which is\n * what the ref version was. The player identity never changes, so state holds it\n * fine and the value is non-null from the first render.\n *\n * Hook-managed audio player. Each component instance gets its own\n * {@link AudioPlayer}, so unmounting cleanly stops playback. Useful for\n * notification chimes, UI feedback sounds, and per-component soundtracks.\n */\nexport function useAudio(): UseAudioResult {\n const [player] = useState<AudioPlayer>(() => createAudioPlayer());\n const [unlocked, setUnlocked] = useState<boolean>(false);\n\n useEffect(() => {\n return () => {\n player.stop();\n };\n }, [player]);\n\n const play = useCallback(\n async (src: string, options?: PlayAudioOptions): Promise<void> => {\n const result = await player.play(src, options);\n if (result) setUnlocked(true);\n },\n [player],\n );\n\n const stop = useCallback((): void => {\n player.stop();\n }, [player]);\n\n return { play, stop, unlocked };\n}\n"],"mappings":";;;AAyBA,SAAgB,IAA2B;CACvC,IAAM,CAAC,KAAU,QAA4B,EAAkB,CAAC,GAC1D,CAAC,GAAU,KAAe,EAAkB,EAAK;CAoBvD,OAlBA,cACiB;EACT,EAAO,KAAK;CAChB,GACD,CAAC,CAAM,CAAC,GAcJ;EAAE,MAZI,EACT,OAAO,GAAa,MAA8C;GAE9D,AAAI,MADiB,EAAO,KAAK,GAAK,CAAO,KACjC,EAAY,EAAI;EAChC,GACA,CAAC,CAAM,CAOF;EAAM,MAJF,QAAwB;GACjC,EAAO,KAAK;EAChB,GAAG,CAAC,CAAM,CAEK;EAAM;CAAS;AAClC"}
1
+ {"version":3,"file":"use-audio.js","names":[],"sources":["../../src/audio/use-audio.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\nimport { createAudioPlayer, type AudioPlayerHandle, type PlayAudioOptions } from \"./audio-player\";\n\nexport interface UseAudioResult {\n /** Play `src` on the hook's private player. */\n play: (src: string, options?: PlayAudioOptions) => Promise<void>;\n /** Stop the current clip. */\n stop: () => void;\n /**\n * Whether the browser's autoplay policy has been satisfied. Becomes\n * `true` after the first successful `play()`.\n */\n unlocked: boolean;\n}\n\n/**\n * The player lives in a state initializer, not in a lazily-written ref: the\n * initializer runs exactly once and is not a side effect during render, which is\n * what the ref version was. The player identity never changes, so state holds it\n * fine and the value is non-null from the first render.\n *\n * Hook-managed audio player. Each component instance gets its own\n * {@link AudioPlayerHandle}, so unmounting cleanly stops playback. Useful for\n * notification chimes, UI feedback sounds, and per-component soundtracks.\n */\nexport function useAudio(): UseAudioResult {\n const [player] = useState<AudioPlayerHandle>(() => createAudioPlayer());\n const [unlocked, setUnlocked] = useState<boolean>(false);\n\n useEffect(() => {\n return () => {\n player.stop();\n };\n }, [player]);\n\n const play = useCallback(\n async (src: string, options?: PlayAudioOptions): Promise<void> => {\n const result = await player.play(src, options);\n if (result) setUnlocked(true);\n },\n [player],\n );\n\n const stop = useCallback((): void => {\n player.stop();\n }, [player]);\n\n return { play, stop, unlocked };\n}\n"],"mappings":";;;AAyBA,SAAgB,IAA2B;CACvC,IAAM,CAAC,KAAU,QAAkC,EAAkB,CAAC,GAChE,CAAC,GAAU,KAAe,EAAkB,EAAK;CAoBvD,OAlBA,cACiB;EACT,EAAO,KAAK;CAChB,GACD,CAAC,CAAM,CAAC,GAcJ;EAAE,MAZI,EACT,OAAO,GAAa,MAA8C;GAE9D,AAAI,MADiB,EAAO,KAAK,GAAK,CAAO,KACjC,EAAY,EAAI;EAChC,GACA,CAAC,CAAM,CAOF;EAAM,MAJF,QAAwB;GACjC,EAAO,KAAK;EAChB,GAAG,CAAC,CAAM,CAEK;EAAM;CAAS;AAClC"}
@@ -0,0 +1,2 @@
1
+ const e=require("./media-access.cjs");let t=require("react");function n(n={}){let{autoStart:r=!1,deviceId:i,echoCancellation:a=!0,noiseSuppression:o=!0,autoGainControl:s=!0,constraints:c}=n,[l,u]=(0,t.useState)(`idle`),[d,f]=(0,t.useState)(null),[p,m]=(0,t.useState)(null),h=(0,t.useRef)(null),g=(0,t.useRef)(0),_=(0,t.useCallback)(()=>{h.current?.getTracks().forEach(e=>e.stop()),h.current=null},[]),v=(0,t.useCallback)(()=>{g.current+=1,_(),f(null),u(`idle`),m(null)},[_]),y=(0,t.useCallback)(()=>{if(h.current)return;let t=g.current+=1;if(u(`requesting`),m(null),!e.isMediaCaptureSupported()){m(e.missingCaptureApiError(`microphone`)),u(`error`);return}let n=c??{audio:{echoCancellation:a,noiseSuppression:o,autoGainControl:s,...i?{deviceId:{exact:i}}:{}},video:!1};navigator.mediaDevices.getUserMedia(n).then(e=>{if(t!==g.current){e.getTracks().forEach(e=>e.stop());return}h.current=e,f(e),u(`ready`)}).catch(n=>{t===g.current&&(m(e.classifyMediaError(n,`microphone`)),u(`error`))})},[c,i,a,o,s]);return(0,t.useEffect)(()=>{r&&y()},[r]),(0,t.useEffect)(()=>_,[_]),{status:l,stream:d,error:p,start:y,stop:v}}exports.useMicrophone=n;
2
+ //# sourceMappingURL=use-microphone.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-microphone.cjs","names":[],"sources":["../../src/audio/use-microphone.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n classifyMediaError,\n isMediaCaptureSupported,\n missingCaptureApiError,\n type MediaAccessError,\n} from \"./media-access\";\n\n/** Lifecycle of a microphone stream. */\nexport type MicrophoneStatus = \"idle\" | \"requesting\" | \"ready\" | \"error\";\n\n/** Options for {@link useMicrophone}. */\nexport interface UseMicrophoneOptions {\n /**\n * Open the stream on mount instead of waiting for `start()`. Default `false`.\n *\n * Left off on purpose. Opening on mount means prompting on mount, and a\n * permission prompt the user did not provoke is the most reliable way to earn a\n * permanent \"Block\" — after which `getUserMedia` rejects without ever prompting\n * again. Wire `start()` to the button that needs the microphone.\n */\n autoStart?: boolean;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Browser voice processing. All three default to `true`, which is what speech\n * wants; turn them off for music, where a gate chewing on a decaying note is\n * worse than the room noise it removes.\n */\n echoCancellation?: boolean;\n noiseSuppression?: boolean;\n autoGainControl?: boolean;\n /** Escape hatch: full constraints, replacing everything above. */\n constraints?: MediaStreamConstraints;\n}\n\n/** Value returned by {@link useMicrophone}. */\nexport interface UseMicrophoneResult {\n status: MicrophoneStatus;\n /** The live stream, or `null`. Feed it to {@link useAudioRecorder} or an analyser. */\n stream: MediaStream | null;\n /** Classified error, or `null` outside the `error` status. */\n error: MediaAccessError | null;\n /** Open the stream. Safe to call when already open — it is a no-op. */\n start: () => void;\n /** Release the microphone. The browser's recording indicator only clears here. */\n stop: () => void;\n}\n\n/**\n * Acquire a microphone `MediaStream`, classified errors included, and release it\n * properly.\n *\n * Releasing matters more than it looks. Every track has to be stopped by hand:\n * dropping the last reference to a `MediaStream` does **not** turn off the\n * microphone, so the browser keeps showing its recording indicator and the OS keeps\n * the device busy — which then makes the *next* `getUserMedia` fail with\n * `NotReadableError` in another tab. The hook stops tracks on `stop()`, on unmount,\n * and before re-opening.\n *\n * @param options - See {@link UseMicrophoneOptions}.\n * @returns The stream, its status, a classified error, and `start`/`stop`.\n *\n * @example\n * const mic = useMicrophone();\n * const recorder = useAudioRecorder(mic.stream);\n * <button onClick={mic.start}>Liberar microfone</button>\n */\nexport function useMicrophone(options: UseMicrophoneOptions = {}): UseMicrophoneResult {\n const {\n autoStart = false,\n deviceId,\n echoCancellation = true,\n noiseSuppression = true,\n autoGainControl = true,\n constraints,\n } = options;\n\n const [status, setStatus] = useState<MicrophoneStatus>(\"idle\");\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [error, setError] = useState<MediaAccessError | null>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const generation = useRef(0);\n\n const release = useCallback((): void => {\n streamRef.current?.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }, []);\n\n const stop = useCallback((): void => {\n generation.current += 1;\n release();\n setStream(null);\n setStatus(\"idle\");\n setError(null);\n }, [release]);\n\n const start = useCallback((): void => {\n if (streamRef.current) return;\n const run = (generation.current += 1);\n setStatus(\"requesting\");\n setError(null);\n\n if (!isMediaCaptureSupported()) {\n setError(missingCaptureApiError(\"microphone\"));\n setStatus(\"error\");\n return;\n }\n\n const request: MediaStreamConstraints = constraints ?? {\n audio: {\n echoCancellation,\n noiseSuppression,\n autoGainControl,\n ...(deviceId ? { deviceId: { exact: deviceId } } : {}),\n },\n video: false,\n };\n\n void navigator.mediaDevices\n .getUserMedia(request)\n .then((next) => {\n if (run !== generation.current) {\n next.getTracks().forEach((track) => track.stop());\n return;\n }\n streamRef.current = next;\n setStream(next);\n setStatus(\"ready\");\n })\n .catch((err: unknown) => {\n if (run !== generation.current) return;\n setError(classifyMediaError(err, \"microphone\"));\n setStatus(\"error\");\n });\n }, [constraints, deviceId, echoCancellation, noiseSuppression, autoGainControl]);\n\n useEffect(() => {\n if (autoStart) start();\n // `start` is stable per constraint set; re-running on every identity change\n // would reopen the device on each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [autoStart]);\n\n useEffect(() => release, [release]);\n\n return { status, stream, error, start, stop };\n}\n"],"mappings":"6DAqEA,SAAgB,EAAc,EAAgC,CAAC,EAAwB,CACnF,GAAM,CACF,YAAY,GACZ,WACA,mBAAmB,GACnB,mBAAmB,GACnB,kBAAkB,GAClB,eACA,EAEE,CAAC,EAAQ,IAAA,EAAA,EAAA,SAAA,CAAwC,MAAM,EACvD,CAAC,EAAQ,IAAA,EAAA,EAAA,SAAA,CAA0C,IAAI,EACvD,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAA8C,IAAI,EAC1D,GAAA,EAAA,EAAA,OAAA,CAAuC,IAAI,EAC3C,GAAA,EAAA,EAAA,OAAA,CAAoB,CAAC,EAErB,GAAA,EAAA,EAAA,YAAA,KAAkC,CACpC,EAAU,SAAS,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAC9D,EAAU,QAAU,IACxB,EAAG,CAAC,CAAC,EAEC,GAAA,EAAA,EAAA,YAAA,KAA+B,CACjC,EAAW,SAAW,EACtB,EAAQ,EACR,EAAU,IAAI,EACd,EAAU,MAAM,EAChB,EAAS,IAAI,CACjB,EAAG,CAAC,CAAO,CAAC,EAEN,GAAA,EAAA,EAAA,YAAA,KAAgC,CAClC,GAAI,EAAU,QAAS,OACvB,IAAM,EAAO,EAAW,SAAW,EAInC,GAHA,EAAU,YAAY,EACtB,EAAS,IAAI,EAET,CAAC,EAAA,wBAAwB,EAAG,CAC5B,EAAS,EAAA,uBAAuB,YAAY,CAAC,EAC7C,EAAU,OAAO,EACjB,MACJ,CAEA,IAAM,EAAkC,GAAe,CACnD,MAAO,CACH,mBACA,mBACA,kBACA,GAAI,EAAW,CAAE,SAAU,CAAE,MAAO,CAAS,CAAE,EAAI,CAAC,CACxD,EACA,MAAO,EACX,EAEA,UAAe,aACV,aAAa,CAAO,CAAC,CACrB,KAAM,GAAS,CACZ,GAAI,IAAQ,EAAW,QAAS,CAC5B,EAAK,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAChD,MACJ,CACA,EAAU,QAAU,EACpB,EAAU,CAAI,EACd,EAAU,OAAO,CACrB,CAAC,CAAC,CACD,MAAO,GAAiB,CACjB,IAAQ,EAAW,UACvB,EAAS,EAAA,mBAAmB,EAAK,YAAY,CAAC,EAC9C,EAAU,OAAO,EACrB,CAAC,CACT,EAAG,CAAC,EAAa,EAAU,EAAkB,EAAkB,CAAe,CAAC,EAW/E,OATA,EAAA,EAAA,UAAA,KAAgB,CACR,GAAW,EAAM,CAIzB,EAAG,CAAC,CAAS,CAAC,GAEd,EAAA,EAAA,UAAA,KAAgB,EAAS,CAAC,CAAO,CAAC,EAE3B,CAAE,SAAQ,SAAQ,QAAO,QAAO,MAAK,CAChD"}
@@ -0,0 +1,54 @@
1
+ import { classifyMediaError as e, isMediaCaptureSupported as t, missingCaptureApiError as n } from "./media-access.js";
2
+ import { useCallback as r, useEffect as i, useRef as a, useState as o } from "react";
3
+ //#region src/audio/use-microphone.ts
4
+ function s(s = {}) {
5
+ let { autoStart: c = !1, deviceId: l, echoCancellation: u = !0, noiseSuppression: d = !0, autoGainControl: f = !0, constraints: p } = s, [m, h] = o("idle"), [g, _] = o(null), [v, y] = o(null), b = a(null), x = a(0), S = r(() => {
6
+ b.current?.getTracks().forEach((e) => e.stop()), b.current = null;
7
+ }, []), C = r(() => {
8
+ x.current += 1, S(), _(null), h("idle"), y(null);
9
+ }, [S]), w = r(() => {
10
+ if (b.current) return;
11
+ let r = x.current += 1;
12
+ if (h("requesting"), y(null), !t()) {
13
+ y(n("microphone")), h("error");
14
+ return;
15
+ }
16
+ let i = p ?? {
17
+ audio: {
18
+ echoCancellation: u,
19
+ noiseSuppression: d,
20
+ autoGainControl: f,
21
+ ...l ? { deviceId: { exact: l } } : {}
22
+ },
23
+ video: !1
24
+ };
25
+ navigator.mediaDevices.getUserMedia(i).then((e) => {
26
+ if (r !== x.current) {
27
+ e.getTracks().forEach((e) => e.stop());
28
+ return;
29
+ }
30
+ b.current = e, _(e), h("ready");
31
+ }).catch((t) => {
32
+ r === x.current && (y(e(t, "microphone")), h("error"));
33
+ });
34
+ }, [
35
+ p,
36
+ l,
37
+ u,
38
+ d,
39
+ f
40
+ ]);
41
+ return i(() => {
42
+ c && w();
43
+ }, [c]), i(() => S, [S]), {
44
+ status: m,
45
+ stream: g,
46
+ error: v,
47
+ start: w,
48
+ stop: C
49
+ };
50
+ }
51
+ //#endregion
52
+ export { s as useMicrophone };
53
+
54
+ //# sourceMappingURL=use-microphone.js.map