tempest-react-sdk 0.38.2 → 0.38.3

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 (291) hide show
  1. package/bin/lib/design/collect.mjs +19 -0
  2. package/bin/lib/design/functions.mjs +5 -0
  3. package/bin/lib/design/index.mjs +8 -3
  4. package/bin/lib/design/index.test.mjs +31 -0
  5. package/bin/lib/design/scan.mjs +34 -2
  6. package/bin/lib/design/scan.test.mjs +78 -0
  7. package/bin/lib/doctor/doctor.e2e.test.mjs +85 -0
  8. package/bin/tempest.mjs +90 -6
  9. package/dist/audio/level-meter.cjs.map +1 -1
  10. package/dist/audio/level-meter.js.map +1 -1
  11. package/dist/audio/use-audio-recorder.cjs.map +1 -1
  12. package/dist/audio/use-audio-recorder.js.map +1 -1
  13. package/dist/auth/passkey.cjs.map +1 -1
  14. package/dist/auth/passkey.js.map +1 -1
  15. package/dist/auth/use-passkey.cjs.map +1 -1
  16. package/dist/auth/use-passkey.js.map +1 -1
  17. package/dist/br/BrazilMap.cjs.map +1 -1
  18. package/dist/br/BrazilMap.js.map +1 -1
  19. package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
  20. package/dist/br/BrazilStateCitySelect.js.map +1 -1
  21. package/dist/br/BrazilStateMap.cjs.map +1 -1
  22. package/dist/br/BrazilStateMap.js.map +1 -1
  23. package/dist/br/PixQRCode.cjs.map +1 -1
  24. package/dist/br/PixQRCode.js.map +1 -1
  25. package/dist/br/boleto.cjs.map +1 -1
  26. package/dist/br/boleto.js.map +1 -1
  27. package/dist/br/pix.cjs.map +1 -1
  28. package/dist/br/pix.js.map +1 -1
  29. package/dist/br.d.ts +8 -0
  30. package/dist/capture/media-recorder.cjs.map +1 -1
  31. package/dist/capture/media-recorder.js.map +1 -1
  32. package/dist/capture/use-barcode-scanner.cjs.map +1 -1
  33. package/dist/capture/use-barcode-scanner.js.map +1 -1
  34. package/dist/capture/use-screen-capture.cjs.map +1 -1
  35. package/dist/capture/use-screen-capture.js.map +1 -1
  36. package/dist/capture/use-speech-recognition.cjs.map +1 -1
  37. package/dist/capture/use-speech-recognition.js.map +1 -1
  38. package/dist/capture/use-video-recorder.cjs.map +1 -1
  39. package/dist/capture/use-video-recorder.js.map +1 -1
  40. package/dist/charts/types.cjs.map +1 -1
  41. package/dist/charts/types.js.map +1 -1
  42. package/dist/charts.d.ts +8 -0
  43. package/dist/components/AIChat/AIChat.cjs.map +1 -1
  44. package/dist/components/AIChat/AIChat.js.map +1 -1
  45. package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
  46. package/dist/components/AIChat/AIChatComposer.js.map +1 -1
  47. package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
  48. package/dist/components/AIChat/AIChatTurn.js.map +1 -1
  49. package/dist/components/AppBar/AppBar.cjs.map +1 -1
  50. package/dist/components/AppBar/AppBar.js.map +1 -1
  51. package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
  52. package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
  53. package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
  54. package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
  55. package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -1
  56. package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -1
  57. package/dist/components/Button/Button.cjs.map +1 -1
  58. package/dist/components/Button/Button.js.map +1 -1
  59. package/dist/components/Calendar/Calendar.cjs.map +1 -1
  60. package/dist/components/Calendar/Calendar.js.map +1 -1
  61. package/dist/components/Carousel/Carousel.cjs.map +1 -1
  62. package/dist/components/Carousel/Carousel.js.map +1 -1
  63. package/dist/components/Chat/Chat.cjs.map +1 -1
  64. package/dist/components/Chat/Chat.js.map +1 -1
  65. package/dist/components/ChipInput/ChipInput.cjs.map +1 -1
  66. package/dist/components/ChipInput/ChipInput.js.map +1 -1
  67. package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
  68. package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
  69. package/dist/components/Combobox/Combobox.cjs.map +1 -1
  70. package/dist/components/Combobox/Combobox.js.map +1 -1
  71. package/dist/components/Command/Command.cjs.map +1 -1
  72. package/dist/components/Command/Command.js.map +1 -1
  73. package/dist/components/ConfirmDialog/ConfirmDialog.cjs.map +1 -1
  74. package/dist/components/ConfirmDialog/ConfirmDialog.js.map +1 -1
  75. package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
  76. package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
  77. package/dist/components/CopyButton/CopyButton.cjs.map +1 -1
  78. package/dist/components/CopyButton/CopyButton.js.map +1 -1
  79. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  80. package/dist/components/DataTable/DataTable.js.map +1 -1
  81. package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
  82. package/dist/components/DataTable/DataTable.module.js.map +1 -1
  83. package/dist/components/DataTable/EditableCell.cjs.map +1 -1
  84. package/dist/components/DataTable/EditableCell.js.map +1 -1
  85. package/dist/components/DatePicker/DatePicker.cjs.map +1 -1
  86. package/dist/components/DatePicker/DatePicker.js.map +1 -1
  87. package/dist/components/DateRangePicker/DateRangePicker.cjs.map +1 -1
  88. package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
  89. package/dist/components/Drawer/Drawer.cjs.map +1 -1
  90. package/dist/components/Drawer/Drawer.js.map +1 -1
  91. package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
  92. package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
  93. package/dist/components/Dropzone/Dropzone.cjs.map +1 -1
  94. package/dist/components/Dropzone/Dropzone.js.map +1 -1
  95. package/dist/components/FileUpload/FileUpload.cjs.map +1 -1
  96. package/dist/components/FileUpload/FileUpload.js.map +1 -1
  97. package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
  98. package/dist/components/FilterBar/FilterBar.js.map +1 -1
  99. package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
  100. package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
  101. package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
  102. package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
  103. package/dist/components/Kanban/Kanban.cjs.map +1 -1
  104. package/dist/components/Kanban/Kanban.js.map +1 -1
  105. package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
  106. package/dist/components/Lightbox/Lightbox.js.map +1 -1
  107. package/dist/components/ListTile/ListTile.cjs.map +1 -1
  108. package/dist/components/ListTile/ListTile.js.map +1 -1
  109. package/dist/components/Markdown/Markdown.cjs.map +1 -1
  110. package/dist/components/Markdown/Markdown.js.map +1 -1
  111. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  112. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  113. package/dist/components/Menubar/Menubar.cjs.map +1 -1
  114. package/dist/components/Menubar/Menubar.js.map +1 -1
  115. package/dist/components/Modal/Modal.cjs.map +1 -1
  116. package/dist/components/Modal/Modal.js.map +1 -1
  117. package/dist/components/ModalsManager/ModalsManager.cjs.map +1 -1
  118. package/dist/components/ModalsManager/ModalsManager.js.map +1 -1
  119. package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
  120. package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
  121. package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
  122. package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
  123. package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -1
  124. package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
  125. package/dist/components/Page/Page.cjs.map +1 -1
  126. package/dist/components/Page/Page.js.map +1 -1
  127. package/dist/components/Pagination/Pagination.cjs.map +1 -1
  128. package/dist/components/Pagination/Pagination.js.map +1 -1
  129. package/dist/components/PasswordInput/PasswordInput.cjs.map +1 -1
  130. package/dist/components/PasswordInput/PasswordInput.js.map +1 -1
  131. package/dist/components/PinInput/PinInput.cjs.map +1 -1
  132. package/dist/components/PinInput/PinInput.js.map +1 -1
  133. package/dist/components/Popover/Popover.cjs.map +1 -1
  134. package/dist/components/Popover/Popover.js.map +1 -1
  135. package/dist/components/Progress/Progress.cjs.map +1 -1
  136. package/dist/components/Progress/Progress.js.map +1 -1
  137. package/dist/components/QRCode/qr-encode.cjs.map +1 -1
  138. package/dist/components/QRCode/qr-encode.js.map +1 -1
  139. package/dist/components/Radio/Radio.cjs.map +1 -1
  140. package/dist/components/Radio/Radio.js.map +1 -1
  141. package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
  142. package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
  143. package/dist/components/RatingStars/RatingStars.cjs.map +1 -1
  144. package/dist/components/RatingStars/RatingStars.js.map +1 -1
  145. package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
  146. package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
  147. package/dist/components/Resizable/Resizable.cjs.map +1 -1
  148. package/dist/components/Resizable/Resizable.js.map +1 -1
  149. package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
  150. package/dist/components/Scheduler/Scheduler.js.map +1 -1
  151. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  152. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  153. package/dist/components/SignaturePad/SignaturePad.cjs.map +1 -1
  154. package/dist/components/SignaturePad/SignaturePad.js.map +1 -1
  155. package/dist/components/Slider/Slider.cjs.map +1 -1
  156. package/dist/components/Slider/Slider.js.map +1 -1
  157. package/dist/components/Sparkline/Sparkline.cjs.map +1 -1
  158. package/dist/components/Sparkline/Sparkline.js.map +1 -1
  159. package/dist/components/StepperInput/StepperInput.cjs.map +1 -1
  160. package/dist/components/StepperInput/StepperInput.js.map +1 -1
  161. package/dist/components/Table/Table.cjs.map +1 -1
  162. package/dist/components/Table/Table.js.map +1 -1
  163. package/dist/components/TimePicker/TimePicker.cjs.map +1 -1
  164. package/dist/components/TimePicker/TimePicker.js.map +1 -1
  165. package/dist/components/Toast/ToastProvider.cjs.map +1 -1
  166. package/dist/components/Toast/ToastProvider.js.map +1 -1
  167. package/dist/components/Tour/Tour.cjs.map +1 -1
  168. package/dist/components/Tour/Tour.js.map +1 -1
  169. package/dist/components/Transfer/Transfer.cjs.map +1 -1
  170. package/dist/components/Transfer/Transfer.js.map +1 -1
  171. package/dist/components/TreeView/TreeView.cjs.map +1 -1
  172. package/dist/components/TreeView/TreeView.js.map +1 -1
  173. package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
  174. package/dist/components/VirtualList/VirtualList.js.map +1 -1
  175. package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
  176. package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
  177. package/dist/components/Wizard/Wizard.cjs.map +1 -1
  178. package/dist/components/Wizard/Wizard.js.map +1 -1
  179. package/dist/editor/RichTextEditor.cjs.map +1 -1
  180. package/dist/editor/RichTextEditor.js.map +1 -1
  181. package/dist/forms/FormField.cjs.map +1 -1
  182. package/dist/forms/FormField.js.map +1 -1
  183. package/dist/geo/TrajectoryMap.cjs.map +1 -1
  184. package/dist/geo/TrajectoryMap.js.map +1 -1
  185. package/dist/geo/estimate.cjs.map +1 -1
  186. package/dist/geo/estimate.js.map +1 -1
  187. package/dist/geo/projection.cjs.map +1 -1
  188. package/dist/geo/projection.js.map +1 -1
  189. package/dist/hooks/use-event-listener.cjs.map +1 -1
  190. package/dist/hooks/use-event-listener.js.map +1 -1
  191. package/dist/hooks/use-local-storage.cjs.map +1 -1
  192. package/dist/hooks/use-local-storage.js.map +1 -1
  193. package/dist/hooks/use-sortable.cjs.map +1 -1
  194. package/dist/hooks/use-sortable.js.map +1 -1
  195. package/dist/http/api-client.cjs.map +1 -1
  196. package/dist/http/api-client.js.map +1 -1
  197. package/dist/http/errors.cjs.map +1 -1
  198. package/dist/http/errors.js.map +1 -1
  199. package/dist/http/resumable-upload.cjs.map +1 -1
  200. package/dist/http/resumable-upload.js.map +1 -1
  201. package/dist/http/upload-with-progress.cjs.map +1 -1
  202. package/dist/http/upload-with-progress.js.map +1 -1
  203. package/dist/i18n/I18nProvider.cjs.map +1 -1
  204. package/dist/i18n/I18nProvider.js.map +1 -1
  205. package/dist/imaging/canvas.cjs.map +1 -1
  206. package/dist/imaging/canvas.js.map +1 -1
  207. package/dist/imaging.d.ts +5 -0
  208. package/dist/logger/logger.cjs.map +1 -1
  209. package/dist/logger/logger.js.map +1 -1
  210. package/dist/oauth/GoogleSignIn.cjs.map +1 -1
  211. package/dist/oauth/GoogleSignIn.js.map +1 -1
  212. package/dist/offline/create-offline-sync.cjs.map +1 -1
  213. package/dist/offline/create-offline-sync.js.map +1 -1
  214. package/dist/perf/cache-size.cjs +1 -1
  215. package/dist/perf/cache-size.cjs.map +1 -1
  216. package/dist/perf/cache-size.js +27 -6
  217. package/dist/perf/cache-size.js.map +1 -1
  218. package/dist/sse/create-event-stream.cjs.map +1 -1
  219. package/dist/sse/create-event-stream.js.map +1 -1
  220. package/dist/styles.css +1 -1
  221. package/dist/sw/background-sync.cjs.map +1 -1
  222. package/dist/sw/background-sync.js.map +1 -1
  223. package/dist/sw/cache.cjs.map +1 -1
  224. package/dist/sw/cache.js.map +1 -1
  225. package/dist/sw.d.ts +12 -0
  226. package/dist/tabular/compact.cjs.map +1 -1
  227. package/dist/tabular/compact.js.map +1 -1
  228. package/dist/tabular/predictor.cjs.map +1 -1
  229. package/dist/tabular/predictor.js.map +1 -1
  230. package/dist/tabular.d.ts +7 -2
  231. package/dist/tempest-react-sdk.d.ts +133 -6
  232. package/dist/theme/ThemeProvider.cjs.map +1 -1
  233. package/dist/theme/ThemeProvider.js.map +1 -1
  234. package/dist/theme/color.cjs.map +1 -1
  235. package/dist/theme/color.js.map +1 -1
  236. package/dist/theme/create-theme.cjs.map +1 -1
  237. package/dist/theme/create-theme.js.map +1 -1
  238. package/dist/theme/data-viz-ramps.cjs +1 -1
  239. package/dist/theme/data-viz-ramps.cjs.map +1 -1
  240. package/dist/theme/data-viz-ramps.js +1 -1
  241. package/dist/theme/data-viz-ramps.js.map +1 -1
  242. package/dist/utils/storage.cjs.map +1 -1
  243. package/dist/utils/storage.js.map +1 -1
  244. package/dist/vision/core/canvas.cjs.map +1 -1
  245. package/dist/vision/core/canvas.js.map +1 -1
  246. package/dist/vision/core/exceptions.cjs.map +1 -1
  247. package/dist/vision/core/exceptions.js.map +1 -1
  248. package/dist/vision/core/graph.cjs.map +1 -1
  249. package/dist/vision/core/graph.js.map +1 -1
  250. package/dist/vision/core/metadata.cjs.map +1 -1
  251. package/dist/vision/core/metadata.js.map +1 -1
  252. package/dist/vision/core/providers.cjs.map +1 -1
  253. package/dist/vision/core/providers.js.map +1 -1
  254. package/dist/vision/core/session.cjs.map +1 -1
  255. package/dist/vision/core/session.js.map +1 -1
  256. package/dist/vision/core/timing.cjs.map +1 -1
  257. package/dist/vision/core/timing.js.map +1 -1
  258. package/dist/vision/io/image.cjs.map +1 -1
  259. package/dist/vision/io/image.js.map +1 -1
  260. package/dist/vision/labels.cjs.map +1 -1
  261. package/dist/vision/labels.js.map +1 -1
  262. package/dist/vision/luminance.cjs.map +1 -1
  263. package/dist/vision/luminance.js.map +1 -1
  264. package/dist/vision/postprocess/classification.cjs.map +1 -1
  265. package/dist/vision/postprocess/classification.js.map +1 -1
  266. package/dist/vision/postprocess/detection.cjs.map +1 -1
  267. package/dist/vision/postprocess/detection.js.map +1 -1
  268. package/dist/vision/postprocess/segmentation.cjs.map +1 -1
  269. package/dist/vision/postprocess/segmentation.js.map +1 -1
  270. package/dist/vision/preprocess/image.cjs.map +1 -1
  271. package/dist/vision/preprocess/image.js.map +1 -1
  272. package/dist/vision/results.cjs.map +1 -1
  273. package/dist/vision/results.js.map +1 -1
  274. package/dist/vision/tasks/base.cjs.map +1 -1
  275. package/dist/vision/tasks/base.js.map +1 -1
  276. package/dist/vision/tasks/classifier.cjs.map +1 -1
  277. package/dist/vision/tasks/classifier.js.map +1 -1
  278. package/dist/vision/tasks/detector.cjs.map +1 -1
  279. package/dist/vision/tasks/detector.js.map +1 -1
  280. package/dist/vision/tasks/segmenter.cjs.map +1 -1
  281. package/dist/vision/tasks/segmenter.js.map +1 -1
  282. package/dist/vision/types.cjs.map +1 -1
  283. package/dist/vision/types.js.map +1 -1
  284. package/dist/vision.d.ts +6 -0
  285. package/dist/vite/tempest-icons.cjs.map +1 -1
  286. package/dist/vite/tempest-icons.js.map +1 -1
  287. package/dist/vite/tempest-pwa-icons.cjs.map +1 -1
  288. package/dist/vite/tempest-pwa-icons.js.map +1 -1
  289. package/dist/ws/create-web-socket.cjs.map +1 -1
  290. package/dist/ws/create-web-socket.js.map +1 -1
  291. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"pix.js","names":[],"sources":["../../src/br/pix.ts"],"sourcesContent":["import { validateCNPJ, validateCPF } from \"@/forms/br-validators\";\n\n/**\n * A Pix payload could not be built or read.\n *\n * Its own class so a caller can tell \"the operator typed a bad key\" apart from a\n * bug, and so an `ErrorBoundary` can render a form error instead of a crash.\n */\nexport class PixError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"PixError\";\n }\n}\n\n/** The five key formats DICT accepts. */\nexport type PixKeyType = \"cpf\" | \"cnpj\" | \"email\" | \"phone\" | \"evp\";\n\n/** A key after normalisation, ready to go into the payload. */\nexport interface NormalizedPixKey {\n type: PixKeyType;\n /** The exact string written into the BR Code. */\n value: string;\n}\n\n/** One TLV as it appeared in a payload, unknown tags included. */\nexport interface PixField {\n /** Two-character tag, e.g. `\"59\"`. */\n id: string;\n /** Raw value, still encoded when the tag is itself a template. */\n value: string;\n}\n\n/** A BR Code that carries the key — the one you print on a poster. */\nexport interface PixStaticInput {\n kind?: \"static\";\n /** CPF, CNPJ, e-mail, phone or EVP. Validated and normalised. */\n key: string;\n /** Payee name. Truncated by the spec at 25 characters — longer throws. */\n merchantName: string;\n /** Payee city. Truncated by the spec at 15 characters — longer throws. */\n merchantCity: string;\n /** Amount in BRL. Omit for a payer-chooses-the-value QR. */\n amount?: number;\n /** Reference the PSP echoes back, `[A-Za-z0-9]{1,25}`. Defaults to `\"***\"`. */\n txid?: string;\n /** Free text shown by some wallets. Goes into tag 26, sub-tag 02. */\n description?: string;\n /** CEP, digits only. Optional tag 61. */\n postalCode?: string;\n /** Single-use QR: sets tag 01 to `\"12\"` instead of `\"11\"`. */\n oneTime?: boolean;\n}\n\n/** A BR Code that carries a URL the wallet fetches to learn the amount. */\nexport interface PixDynamicInput {\n kind: \"dynamic\";\n /**\n * `payloadLocation` — the https URL the wallet GETs, **without** the scheme,\n * exactly as BACEN specifies (`pix.example.com/qr/v2/abc`).\n */\n url: string;\n merchantName: string;\n merchantCity: string;\n postalCode?: string;\n /** Single-use QR. Defaults to `true`, which is what a dynamic QR normally is. */\n oneTime?: boolean;\n}\n\n/** Everything `pixPayload` accepts. */\nexport type PixInput = PixStaticInput | PixDynamicInput;\n\n/** A payload taken apart again. */\nexport interface PixData {\n /** `\"dynamic\"` when tag 26 carried a URL instead of a key. */\n kind: \"static\" | \"dynamic\";\n /** Present on a static payload. */\n key?: string;\n keyType?: PixKeyType;\n /** Present on a dynamic payload. */\n url?: string;\n merchantName: string;\n merchantCity: string;\n /** Reais. `undefined` when the payer chooses the amount. */\n amount?: number;\n /** ISO 4217 numeric. `\"986\"` for BRL. */\n currency: string;\n countryCode: string;\n merchantCategoryCode: string;\n /** `\"***\"` on a reusable static QR that identifies no single transaction. */\n txid?: string;\n description?: string;\n postalCode?: string;\n /** Tag 01 read as `\"12\"`. */\n oneTime: boolean;\n /** The four hex characters that closed the payload. */\n crc: string;\n /** Whether those four characters match a recomputed CRC. */\n crcValid: boolean;\n /** Every top-level TLV, in payload order, unknown tags included. */\n fields: PixField[];\n}\n\n/** Options for {@link parsePixPayload}. */\nexport interface ParsePixOptions {\n /**\n * Throw when the checksum does not match. Default `true`.\n *\n * Turn it off only to inspect a payload you already know is broken: a BR Code\n * whose CRC fails has been corrupted in transit, and the account it now points\n * at is not the account the payee published.\n */\n requireCrc?: boolean;\n}\n\n/**\n * Generator polynomial of CRC-16/CCITT-FALSE, `x^16 + x^12 + x^5 + 1`.\n *\n * Taken from the CRC catalogue entry `CRC-16/IBM-3740` (alias CCITT-FALSE):\n * `width=16 poly=0x1021 init=0xffff refin=false refout=false xorout=0x0000\n * check=0x29b1`. The BACEN \"Manual de Padrões para Iniciação do Pix\" names this\n * exact variant for tag 63.\n */\nconst CRC16_POLYNOMIAL = 0x1021;\n\n/** Register preset of CRC-16/CCITT-FALSE. Not zero — that is a different variant. */\nconst CRC16_INITIAL = 0xffff;\n\n/** Keeps the shift register 16 bits wide. */\nconst CRC16_MASK = 0xffff;\n\nconst TAG_PAYLOAD_FORMAT = \"00\";\nconst TAG_POINT_OF_INITIATION = \"01\";\nconst TAG_MERCHANT_ACCOUNT_INFO = \"26\";\nconst TAG_MERCHANT_CATEGORY_CODE = \"52\";\nconst TAG_TRANSACTION_CURRENCY = \"53\";\nconst TAG_TRANSACTION_AMOUNT = \"54\";\nconst TAG_COUNTRY_CODE = \"58\";\nconst TAG_MERCHANT_NAME = \"59\";\nconst TAG_MERCHANT_CITY = \"60\";\nconst TAG_POSTAL_CODE = \"61\";\nconst TAG_ADDITIONAL_DATA = \"62\";\nconst TAG_CRC = \"63\";\n\nconst MAI_TAG_GUI = \"00\";\nconst MAI_TAG_KEY = \"01\";\nconst MAI_TAG_DESCRIPTION = \"02\";\nconst MAI_TAG_URL = \"25\";\nconst ADDITIONAL_TAG_TXID = \"05\";\n\n/** Globally Unique Identifier that marks tag 26 as a Pix account. */\nconst PIX_GUI = \"br.gov.bcb.pix\";\n\nconst PAYLOAD_FORMAT_VERSION = \"01\";\nconst POINT_OF_INITIATION_REUSABLE = \"11\";\nconst POINT_OF_INITIATION_SINGLE_USE = \"12\";\nconst DEFAULT_MERCHANT_CATEGORY_CODE = \"0000\";\nconst CURRENCY_BRL = \"986\";\nconst COUNTRY_BR = \"BR\";\nconst TXID_UNSPECIFIED = \"***\";\n\nconst MAX_MERCHANT_NAME = 25;\nconst MAX_MERCHANT_CITY = 15;\nconst MAX_TXID = 25;\nconst MAX_TLV_VALUE = 99;\nconst MAX_EMAIL_KEY = 77;\n\n/**\n * CRC-16/CCITT-FALSE of a string, as four upper-case hex characters.\n *\n * Bitwise rather than table-driven: 200-odd characters at 8 shifts each is\n * nothing, and a 256-entry table is 2 KB of payload every consumer of the `/br`\n * entry would carry.\n *\n * The input is taken as UTF-8 bytes. A Pix payload is ASCII by construction —\n * {@link pixPayload} rejects anything else — but the function is exported and a\n * caller may hand it arbitrary text, and hashing UTF-16 code units would then\n * disagree with every other implementation.\n *\n * @param input - Bytes to run through the register.\n * @returns Four upper-case hex characters, zero-padded.\n *\n * @example\n * pixCrc16(\"123456789\"); // \"29B1\" — the catalogue check value\n */\nexport function pixCrc16(input: string): string {\n const bytes = new TextEncoder().encode(input);\n let crc = CRC16_INITIAL;\n for (const byte of bytes) {\n crc ^= byte << 8;\n for (let bit = 0; bit < 8; bit += 1) {\n crc =\n (crc & 0x8000) !== 0\n ? ((crc << 1) ^ CRC16_POLYNOMIAL) & CRC16_MASK\n : (crc << 1) & CRC16_MASK;\n }\n }\n return crc.toString(16).toUpperCase().padStart(4, \"0\");\n}\n\n/** Digits of a possibly masked value. */\nfunction digits(value: string): string {\n return value.replace(/\\D/g, \"\");\n}\n\n/**\n * Drop diacritics and reject whatever is left outside printable ASCII.\n *\n * The BR Code character set has no room for accents, and a wallet that meets one\n * either fails to parse the QR or shows mojibake. Stripping them is lossy but\n * legible (\"São Paulo\" → \"Sao Paulo\"), which beats both alternatives; anything\n * that is not a diacritic — an emoji, a CJK character — is a mistake the caller\n * has to see, so it throws.\n */\nfunction toPayloadText(value: string, field: string): string {\n const stripped = value.normalize(\"NFD\").replace(/\\p{Diacritic}/gu, \"\");\n if (/[^\\x20-\\x7E]/.test(stripped)) {\n throw new PixError(\n `${field} has characters the BR Code cannot carry: ${JSON.stringify(value)}. ` +\n \"Use unaccented ASCII.\",\n );\n }\n return stripped;\n}\n\n/** One `ID + 2-digit length + value` triple. */\nfunction tlv(id: string, value: string): string {\n if (value.length > MAX_TLV_VALUE) {\n throw new PixError(\n `Tag ${id} is ${value.length} characters; the EMV length field holds at most ${MAX_TLV_VALUE}.`,\n );\n }\n return `${id}${String(value.length).padStart(2, \"0\")}${value}`;\n}\n\n/**\n * Classify a Pix key, or `null` when it matches no accepted format.\n *\n * !!! warning \"CPF and a national phone number are both eleven digits\"\n * `\"11987654321\"` is a valid mobile number and could be a CPF. The check\n * digits break the tie: an 11-digit string is a CPF when its DV validates and\n * a phone otherwise. Pass phone keys as `+5511987654321` to remove the guess\n * entirely.\n *\n * @param key - Raw key, masked or not.\n * @returns The key type, or `null`.\n */\nexport function pixKeyType(key: string): PixKeyType | null {\n const trimmed = key.trim();\n if (trimmed === \"\") return null;\n\n if (trimmed.includes(\"@\")) {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/.test(trimmed) && trimmed.length <= MAX_EMAIL_KEY\n ? \"email\"\n : null;\n }\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmed)) {\n return \"evp\";\n }\n\n const numbers = digits(trimmed);\n if (numbers.length === 14) return validateCNPJ(numbers) ? \"cnpj\" : null;\n if (numbers.length === 11 && validateCPF(numbers)) return \"cpf\";\n if (trimmed.startsWith(\"+\")) {\n return /^\\+55\\d{10,11}$/.test(`+${numbers}`) ? \"phone\" : null;\n }\n if (numbers.length === 10 || numbers.length === 11) return \"phone\";\n if (numbers.length === 12 || numbers.length === 13) {\n return numbers.startsWith(\"55\") ? \"phone\" : null;\n }\n return null;\n}\n\n/**\n * Validate a Pix key and return the exact string to write into the payload.\n *\n * Normalisation per type: CPF and CNPJ lose their mask, a phone becomes E.164\n * with the `+55` country code, an EVP is lower-cased, and an e-mail is\n * lower-cased because DICT stores it that way — a key that differs only in case\n * would otherwise fail to resolve.\n *\n * All validation lives in {@link pixKeyType}; past that gate the normalisation is\n * total, which is why the phone branch strips a country code purely on length\n * rather than re-checking the shape.\n *\n * @param key - Raw key, masked or not.\n * @returns The type and the normalised value.\n * @throws {PixError} When the key matches no accepted format, or when a document\n * key fails its check digits.\n *\n * @example\n * normalizePixKey(\"123.456.789-09\"); // { type: \"cpf\", value: \"12345678909\" }\n * normalizePixKey(\"(11) 98765-4321\"); // { type: \"phone\", value: \"+5511987654321\" }\n */\nexport function normalizePixKey(key: string): NormalizedPixKey {\n const trimmed = key.trim();\n const type = pixKeyType(trimmed);\n if (type === null) {\n throw new PixError(\n `Not a Pix key: ${JSON.stringify(key)}. Expected a CPF, CNPJ, e-mail, ` +\n \"phone (+5511987654321) or EVP (UUID).\",\n );\n }\n\n if (type === \"email\") return { type, value: trimmed.toLowerCase() };\n if (type === \"evp\") return { type, value: trimmed.toLowerCase() };\n if (type === \"phone\") {\n const numbers = digits(trimmed);\n return { type, value: `+55${numbers.length > 11 ? numbers.slice(2) : numbers}` };\n }\n return { type, value: digits(trimmed) };\n}\n\n/** Format an amount the way tag 54 wants it: dot separator, two decimals. */\nfunction toAmountField(amount: number): string {\n if (!Number.isFinite(amount)) {\n throw new PixError(`Amount must be a finite number, got ${amount}.`);\n }\n if (amount <= 0) {\n throw new PixError(\n `Amount must be positive, got ${amount}. Omit \\`amount\\` for a QR whose value the payer types.`,\n );\n }\n const field = amount.toFixed(2);\n if (field.length > 13) {\n throw new PixError(`Amount ${field} does not fit tag 54, which holds 13 characters.`);\n }\n return field;\n}\n\n/** Bound the two free-text identity fields the spec caps hard. */\nfunction toBoundedText(value: string, max: number, field: string): string {\n const text = toPayloadText(value.trim(), field);\n if (text === \"\") throw new PixError(`${field} is required.`);\n if (text.length > max) {\n throw new PixError(\n `${field} is ${text.length} characters; the BR Code allows ${max}. Shorten it — ` +\n \"truncating here would silently change what the payer sees.\",\n );\n }\n return text;\n}\n\n/** Tag 62, which exists only to carry the txid. */\nfunction additionalDataField(txid: string | undefined): string {\n const value = txid?.trim() ?? \"\";\n if (value === \"\" || value === TXID_UNSPECIFIED) {\n return tlv(TAG_ADDITIONAL_DATA, tlv(ADDITIONAL_TAG_TXID, TXID_UNSPECIFIED));\n }\n if (!new RegExp(`^[A-Za-z0-9]{1,${MAX_TXID}}$`).test(value)) {\n throw new PixError(\n `txid must be 1 to ${MAX_TXID} letters or digits, got ${JSON.stringify(txid)}.`,\n );\n }\n return tlv(TAG_ADDITIONAL_DATA, tlv(ADDITIONAL_TAG_TXID, value));\n}\n\n/** Tag 26 for a static payload: GUI, key and the optional description. */\nfunction staticMerchantAccount(input: PixStaticInput): string {\n const { value } = normalizePixKey(input.key);\n let inner = tlv(MAI_TAG_GUI, PIX_GUI) + tlv(MAI_TAG_KEY, value);\n const description = input.description?.trim();\n if (description !== undefined && description !== \"\") {\n inner += tlv(MAI_TAG_DESCRIPTION, toPayloadText(description, \"description\"));\n }\n if (inner.length > MAX_TLV_VALUE) {\n throw new PixError(\n `Tag 26 is ${inner.length} characters, over the ${MAX_TLV_VALUE} the length field allows. ` +\n \"Shorten `description`.\",\n );\n }\n return tlv(TAG_MERCHANT_ACCOUNT_INFO, inner);\n}\n\n/** Tag 26 for a dynamic payload: GUI plus the URL, and no key. */\nfunction dynamicMerchantAccount(input: PixDynamicInput): string {\n const url = toPayloadText(input.url.trim(), \"url\").replace(/^https?:\\/\\//i, \"\");\n if (url === \"\") throw new PixError(\"url is required for a dynamic BR Code.\");\n const inner = tlv(MAI_TAG_GUI, PIX_GUI) + tlv(MAI_TAG_URL, url);\n if (inner.length > MAX_TLV_VALUE) {\n throw new PixError(\n `Tag 26 is ${inner.length} characters, over the ${MAX_TLV_VALUE} the length field allows. ` +\n \"Shorten the payload URL.\",\n );\n }\n return tlv(TAG_MERCHANT_ACCOUNT_INFO, inner);\n}\n\n/**\n * Build a Pix \"Copia e Cola\" payload — the string behind a Pix QR code.\n *\n * The format is EMVCo MPM: a flat list of `ID + 2-digit length + value` triples,\n * closed by tag 63 holding a CRC-16/CCITT-FALSE over **everything before it,\n * including the literal `6304` header of tag 63 itself**. That last detail is the\n * one implementations get wrong; see {@link pixCrc16}.\n *\n * Two shapes come out of here:\n *\n * - **static** — tag 26 carries the key, so the QR is self-contained and can be\n * printed. Amount optional; a txid of `\"***\"` means \"identifies no single\n * transaction\", which is what a reusable poster QR wants.\n * - **dynamic** — tag 26 carries a URL instead, and the wallet fetches the amount\n * and payee from the PSP. Use it when the value is per-order. Defaults to\n * single-use (tag 01 = `12`).\n *\n * The distinction matters and is not cosmetic: a static QR settles against\n * whatever the payer typed, a dynamic one against what the PSP served, so a\n * charge that must reconcile to a cent needs the dynamic form.\n *\n * @param input - Static or dynamic payload description.\n * @returns The full payload, CRC included, ready to render as a QR or to copy.\n * @throws {PixError} On an unrecognised key, a field over its length cap, a\n * non-positive amount, or text that is not representable in the BR Code.\n *\n * @example\n * pixPayload({\n * key: \"12345678909\",\n * merchantName: \"Loja Tempest\",\n * merchantCity: \"São Paulo\",\n * amount: 25.5,\n * txid: \"PEDIDO123\",\n * });\n */\nexport function pixPayload(input: PixInput): string {\n const dynamic = input.kind === \"dynamic\";\n const oneTime = input.oneTime ?? dynamic;\n\n let payload = tlv(TAG_PAYLOAD_FORMAT, PAYLOAD_FORMAT_VERSION);\n payload += tlv(\n TAG_POINT_OF_INITIATION,\n oneTime ? POINT_OF_INITIATION_SINGLE_USE : POINT_OF_INITIATION_REUSABLE,\n );\n payload += dynamic ? dynamicMerchantAccount(input) : staticMerchantAccount(input);\n payload += tlv(TAG_MERCHANT_CATEGORY_CODE, DEFAULT_MERCHANT_CATEGORY_CODE);\n payload += tlv(TAG_TRANSACTION_CURRENCY, CURRENCY_BRL);\n if (!dynamic && input.amount !== undefined) {\n payload += tlv(TAG_TRANSACTION_AMOUNT, toAmountField(input.amount));\n }\n payload += tlv(TAG_COUNTRY_CODE, COUNTRY_BR);\n payload += tlv(\n TAG_MERCHANT_NAME,\n toBoundedText(input.merchantName, MAX_MERCHANT_NAME, \"merchantName\"),\n );\n payload += tlv(\n TAG_MERCHANT_CITY,\n toBoundedText(input.merchantCity, MAX_MERCHANT_CITY, \"merchantCity\"),\n );\n const postalCode = input.postalCode === undefined ? \"\" : digits(input.postalCode);\n if (postalCode !== \"\") payload += tlv(TAG_POSTAL_CODE, postalCode);\n payload += additionalDataField(dynamic ? TXID_UNSPECIFIED : input.txid);\n\n const withCrcHeader = `${payload}${TAG_CRC}04`;\n return `${withCrcHeader}${pixCrc16(withCrcHeader)}`;\n}\n\n/**\n * Split a flat run of TLVs. Stops cleanly at the first malformed triple.\n *\n * @throws {PixError} When a length prefix is not two digits or runs past the end.\n */\nfunction parseTlv(input: string, where: string): PixField[] {\n const fields: PixField[] = [];\n let cursor = 0;\n while (cursor < input.length) {\n const id = input.slice(cursor, cursor + 2);\n const rawLength = input.slice(cursor + 2, cursor + 4);\n if (!/^\\d{2}$/.test(id) || !/^\\d{2}$/.test(rawLength)) {\n throw new PixError(\n `Malformed TLV in ${where} at offset ${cursor}: expected a 2-digit tag and length.`,\n );\n }\n const length = Number(rawLength);\n const start = cursor + 4;\n if (start + length > input.length) {\n throw new PixError(\n `Tag ${id} in ${where} declares ${length} characters but only ${input.length - start} remain.`,\n );\n }\n fields.push({ id, value: input.slice(start, start + length) });\n cursor = start + length;\n }\n return fields;\n}\n\n/** First value for a tag, or `undefined`. */\nfunction pick(fields: readonly PixField[], id: string): string | undefined {\n return fields.find((field) => field.id === id)?.value;\n}\n\n/**\n * Read a Pix \"Copia e Cola\" payload back into its parts.\n *\n * Tolerant by design: tags the SDK does not know about are kept verbatim in\n * {@link PixData.fields} instead of raising, because PSPs do add their own\n * templates and a reader that rejects them is useless in production. What is\n * *not* tolerated is a broken frame — a length prefix that runs off the end, a\n * missing tag 63 — or a checksum mismatch, which means the string was corrupted\n * and no longer names the account the payee published.\n *\n * @param payload - The copia-e-cola string. Surrounding whitespace is ignored.\n * @param options - See {@link ParsePixOptions}.\n * @returns The decoded payload.\n * @throws {PixError} On a malformed frame, a missing CRC tag, a tag 26 that is\n * not a Pix account, or — unless `requireCrc` is `false` — a CRC mismatch.\n *\n * @example\n * const data = parsePixPayload(copied);\n * console.log(data.key, data.amount, data.txid);\n */\nexport function parsePixPayload(payload: string, options: ParsePixOptions = {}): PixData {\n const { requireCrc = true } = options;\n const text = payload.trim();\n if (text.length < 8) throw new PixError(\"Payload is too short to be a BR Code.\");\n\n const crcHeaderAt = text.length - 8;\n if (text.slice(crcHeaderAt, crcHeaderAt + 4) !== `${TAG_CRC}04`) {\n throw new PixError(\"Payload does not end in a `6304` CRC tag.\");\n }\n const crc = text.slice(-4).toUpperCase();\n const expected = pixCrc16(text.slice(0, -4));\n const crcValid = crc === expected;\n if (!crcValid && requireCrc) {\n throw new PixError(`CRC mismatch: payload says ${crc}, recomputed ${expected}.`);\n }\n\n const fields = parseTlv(text.slice(0, crcHeaderAt), \"payload\");\n const merchantAccount = pick(fields, TAG_MERCHANT_ACCOUNT_INFO);\n if (merchantAccount === undefined) {\n throw new PixError(\"Payload has no tag 26 (merchant account information).\");\n }\n const account = parseTlv(merchantAccount, \"tag 26\");\n const gui = pick(account, MAI_TAG_GUI);\n if (gui?.toLowerCase() !== PIX_GUI) {\n throw new PixError(\n `Tag 26 is not a Pix account: expected GUI ${PIX_GUI}, got ${JSON.stringify(gui)}.`,\n );\n }\n\n const url = pick(account, MAI_TAG_URL);\n const key = pick(account, MAI_TAG_KEY);\n const amountField = pick(fields, TAG_TRANSACTION_AMOUNT);\n const txid = pick(\n parseTlv(pick(fields, TAG_ADDITIONAL_DATA) ?? \"\", \"tag 62\"),\n ADDITIONAL_TAG_TXID,\n );\n\n return {\n kind: url !== undefined && key === undefined ? \"dynamic\" : \"static\",\n ...(key === undefined ? {} : { key, keyType: pixKeyType(key) ?? undefined }),\n ...(url === undefined ? {} : { url }),\n merchantName: pick(fields, TAG_MERCHANT_NAME) ?? \"\",\n merchantCity: pick(fields, TAG_MERCHANT_CITY) ?? \"\",\n ...(amountField === undefined ? {} : { amount: Number(amountField) }),\n currency: pick(fields, TAG_TRANSACTION_CURRENCY) ?? \"\",\n countryCode: pick(fields, TAG_COUNTRY_CODE) ?? \"\",\n merchantCategoryCode: pick(fields, TAG_MERCHANT_CATEGORY_CODE) ?? \"\",\n ...(txid === undefined ? {} : { txid }),\n ...(pick(account, MAI_TAG_DESCRIPTION) === undefined\n ? {}\n : { description: pick(account, MAI_TAG_DESCRIPTION) }),\n ...(pick(fields, TAG_POSTAL_CODE) === undefined\n ? {}\n : { postalCode: pick(fields, TAG_POSTAL_CODE) }),\n oneTime: pick(fields, TAG_POINT_OF_INITIATION) === POINT_OF_INITIATION_SINGLE_USE,\n crc,\n crcValid,\n fields,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pix.js","names":[],"sources":["../../src/br/pix.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — EMV®QRCPS-MPM for Pix: the TLV writer, the\n * CRC16-CCITT, the field catalogue with its nesting, key-type detection and the\n * parser that has to accept what other banks emit. Reader and writer live together\n * because each one is the other's test — a payload this file builds is a payload it\n * must parse back.\n */\nimport { validateCNPJ, validateCPF } from \"@/forms/br-validators\";\n\n/**\n * A Pix payload could not be built or read.\n *\n * Its own class so a caller can tell \"the operator typed a bad key\" apart from a\n * bug, and so an `ErrorBoundary` can render a form error instead of a crash.\n */\nexport class PixError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"PixError\";\n }\n}\n\n/** The five key formats DICT accepts. */\nexport type PixKeyType = \"cpf\" | \"cnpj\" | \"email\" | \"phone\" | \"evp\";\n\n/** A key after normalisation, ready to go into the payload. */\nexport interface NormalizedPixKey {\n type: PixKeyType;\n /** The exact string written into the BR Code. */\n value: string;\n}\n\n/** One TLV as it appeared in a payload, unknown tags included. */\nexport interface PixField {\n /** Two-character tag, e.g. `\"59\"`. */\n id: string;\n /** Raw value, still encoded when the tag is itself a template. */\n value: string;\n}\n\n/** A BR Code that carries the key — the one you print on a poster. */\nexport interface PixStaticInput {\n kind?: \"static\";\n /** CPF, CNPJ, e-mail, phone or EVP. Validated and normalised. */\n key: string;\n /** Payee name. Truncated by the spec at 25 characters — longer throws. */\n merchantName: string;\n /** Payee city. Truncated by the spec at 15 characters — longer throws. */\n merchantCity: string;\n /** Amount in BRL. Omit for a payer-chooses-the-value QR. */\n amount?: number;\n /** Reference the PSP echoes back, `[A-Za-z0-9]{1,25}`. Defaults to `\"***\"`. */\n txid?: string;\n /** Free text shown by some wallets. Goes into tag 26, sub-tag 02. */\n description?: string;\n /** CEP, digits only. Optional tag 61. */\n postalCode?: string;\n /** Single-use QR: sets tag 01 to `\"12\"` instead of `\"11\"`. */\n oneTime?: boolean;\n}\n\n/** A BR Code that carries a URL the wallet fetches to learn the amount. */\nexport interface PixDynamicInput {\n kind: \"dynamic\";\n /**\n * `payloadLocation` — the https URL the wallet GETs, **without** the scheme,\n * exactly as BACEN specifies (`pix.example.com/qr/v2/abc`).\n */\n url: string;\n merchantName: string;\n merchantCity: string;\n postalCode?: string;\n /** Single-use QR. Defaults to `true`, which is what a dynamic QR normally is. */\n oneTime?: boolean;\n}\n\n/** Everything `pixPayload` accepts. */\nexport type PixInput = PixStaticInput | PixDynamicInput;\n\n/** A payload taken apart again. */\nexport interface PixData {\n /** `\"dynamic\"` when tag 26 carried a URL instead of a key. */\n kind: \"static\" | \"dynamic\";\n /** Present on a static payload. */\n key?: string;\n keyType?: PixKeyType;\n /** Present on a dynamic payload. */\n url?: string;\n merchantName: string;\n merchantCity: string;\n /** Reais. `undefined` when the payer chooses the amount. */\n amount?: number;\n /** ISO 4217 numeric. `\"986\"` for BRL. */\n currency: string;\n countryCode: string;\n merchantCategoryCode: string;\n /** `\"***\"` on a reusable static QR that identifies no single transaction. */\n txid?: string;\n description?: string;\n postalCode?: string;\n /** Tag 01 read as `\"12\"`. */\n oneTime: boolean;\n /** The four hex characters that closed the payload. */\n crc: string;\n /** Whether those four characters match a recomputed CRC. */\n crcValid: boolean;\n /** Every top-level TLV, in payload order, unknown tags included. */\n fields: PixField[];\n}\n\n/** Options for {@link parsePixPayload}. */\nexport interface ParsePixOptions {\n /**\n * Throw when the checksum does not match. Default `true`.\n *\n * Turn it off only to inspect a payload you already know is broken: a BR Code\n * whose CRC fails has been corrupted in transit, and the account it now points\n * at is not the account the payee published.\n */\n requireCrc?: boolean;\n}\n\n/**\n * Generator polynomial of CRC-16/CCITT-FALSE, `x^16 + x^12 + x^5 + 1`.\n *\n * Taken from the CRC catalogue entry `CRC-16/IBM-3740` (alias CCITT-FALSE):\n * `width=16 poly=0x1021 init=0xffff refin=false refout=false xorout=0x0000\n * check=0x29b1`. The BACEN \"Manual de Padrões para Iniciação do Pix\" names this\n * exact variant for tag 63.\n */\nconst CRC16_POLYNOMIAL = 0x1021;\n\n/** Register preset of CRC-16/CCITT-FALSE. Not zero — that is a different variant. */\nconst CRC16_INITIAL = 0xffff;\n\n/** Keeps the shift register 16 bits wide. */\nconst CRC16_MASK = 0xffff;\n\nconst TAG_PAYLOAD_FORMAT = \"00\";\nconst TAG_POINT_OF_INITIATION = \"01\";\nconst TAG_MERCHANT_ACCOUNT_INFO = \"26\";\nconst TAG_MERCHANT_CATEGORY_CODE = \"52\";\nconst TAG_TRANSACTION_CURRENCY = \"53\";\nconst TAG_TRANSACTION_AMOUNT = \"54\";\nconst TAG_COUNTRY_CODE = \"58\";\nconst TAG_MERCHANT_NAME = \"59\";\nconst TAG_MERCHANT_CITY = \"60\";\nconst TAG_POSTAL_CODE = \"61\";\nconst TAG_ADDITIONAL_DATA = \"62\";\nconst TAG_CRC = \"63\";\n\nconst MAI_TAG_GUI = \"00\";\nconst MAI_TAG_KEY = \"01\";\nconst MAI_TAG_DESCRIPTION = \"02\";\nconst MAI_TAG_URL = \"25\";\nconst ADDITIONAL_TAG_TXID = \"05\";\n\n/** Globally Unique Identifier that marks tag 26 as a Pix account. */\nconst PIX_GUI = \"br.gov.bcb.pix\";\n\nconst PAYLOAD_FORMAT_VERSION = \"01\";\nconst POINT_OF_INITIATION_REUSABLE = \"11\";\nconst POINT_OF_INITIATION_SINGLE_USE = \"12\";\nconst DEFAULT_MERCHANT_CATEGORY_CODE = \"0000\";\nconst CURRENCY_BRL = \"986\";\nconst COUNTRY_BR = \"BR\";\nconst TXID_UNSPECIFIED = \"***\";\n\nconst MAX_MERCHANT_NAME = 25;\nconst MAX_MERCHANT_CITY = 15;\nconst MAX_TXID = 25;\nconst MAX_TLV_VALUE = 99;\nconst MAX_EMAIL_KEY = 77;\n\n/**\n * CRC-16/CCITT-FALSE of a string, as four upper-case hex characters.\n *\n * Bitwise rather than table-driven: 200-odd characters at 8 shifts each is\n * nothing, and a 256-entry table is 2 KB of payload every consumer of the `/br`\n * entry would carry.\n *\n * The input is taken as UTF-8 bytes. A Pix payload is ASCII by construction —\n * {@link pixPayload} rejects anything else — but the function is exported and a\n * caller may hand it arbitrary text, and hashing UTF-16 code units would then\n * disagree with every other implementation.\n *\n * @param input - Bytes to run through the register.\n * @returns Four upper-case hex characters, zero-padded.\n *\n * @example\n * pixCrc16(\"123456789\"); // \"29B1\" — the catalogue check value\n */\nexport function pixCrc16(input: string): string {\n const bytes = new TextEncoder().encode(input);\n let crc = CRC16_INITIAL;\n for (const byte of bytes) {\n crc ^= byte << 8;\n for (let bit = 0; bit < 8; bit += 1) {\n crc =\n (crc & 0x8000) !== 0\n ? ((crc << 1) ^ CRC16_POLYNOMIAL) & CRC16_MASK\n : (crc << 1) & CRC16_MASK;\n }\n }\n return crc.toString(16).toUpperCase().padStart(4, \"0\");\n}\n\n/** Digits of a possibly masked value. */\nfunction digits(value: string): string {\n return value.replace(/\\D/g, \"\");\n}\n\n/**\n * Drop diacritics and reject whatever is left outside printable ASCII.\n *\n * The BR Code character set has no room for accents, and a wallet that meets one\n * either fails to parse the QR or shows mojibake. Stripping them is lossy but\n * legible (\"São Paulo\" → \"Sao Paulo\"), which beats both alternatives; anything\n * that is not a diacritic — an emoji, a CJK character — is a mistake the caller\n * has to see, so it throws.\n */\nfunction toPayloadText(value: string, field: string): string {\n const stripped = value.normalize(\"NFD\").replace(/\\p{Diacritic}/gu, \"\");\n if (/[^\\x20-\\x7E]/.test(stripped)) {\n throw new PixError(\n `${field} has characters the BR Code cannot carry: ${JSON.stringify(value)}. ` +\n \"Use unaccented ASCII.\",\n );\n }\n return stripped;\n}\n\n/** One `ID + 2-digit length + value` triple. */\nfunction tlv(id: string, value: string): string {\n if (value.length > MAX_TLV_VALUE) {\n throw new PixError(\n `Tag ${id} is ${value.length} characters; the EMV length field holds at most ${MAX_TLV_VALUE}.`,\n );\n }\n return `${id}${String(value.length).padStart(2, \"0\")}${value}`;\n}\n\n/**\n * Classify a Pix key, or `null` when it matches no accepted format.\n *\n * !!! warning \"CPF and a national phone number are both eleven digits\"\n * `\"11987654321\"` is a valid mobile number and could be a CPF. The check\n * digits break the tie: an 11-digit string is a CPF when its DV validates and\n * a phone otherwise. Pass phone keys as `+5511987654321` to remove the guess\n * entirely.\n *\n * @param key - Raw key, masked or not.\n * @returns The key type, or `null`.\n */\nexport function pixKeyType(key: string): PixKeyType | null {\n const trimmed = key.trim();\n if (trimmed === \"\") return null;\n\n if (trimmed.includes(\"@\")) {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/.test(trimmed) && trimmed.length <= MAX_EMAIL_KEY\n ? \"email\"\n : null;\n }\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmed)) {\n return \"evp\";\n }\n\n const numbers = digits(trimmed);\n if (numbers.length === 14) return validateCNPJ(numbers) ? \"cnpj\" : null;\n if (numbers.length === 11 && validateCPF(numbers)) return \"cpf\";\n if (trimmed.startsWith(\"+\")) {\n return /^\\+55\\d{10,11}$/.test(`+${numbers}`) ? \"phone\" : null;\n }\n if (numbers.length === 10 || numbers.length === 11) return \"phone\";\n if (numbers.length === 12 || numbers.length === 13) {\n return numbers.startsWith(\"55\") ? \"phone\" : null;\n }\n return null;\n}\n\n/**\n * Validate a Pix key and return the exact string to write into the payload.\n *\n * Normalisation per type: CPF and CNPJ lose their mask, a phone becomes E.164\n * with the `+55` country code, an EVP is lower-cased, and an e-mail is\n * lower-cased because DICT stores it that way — a key that differs only in case\n * would otherwise fail to resolve.\n *\n * All validation lives in {@link pixKeyType}; past that gate the normalisation is\n * total, which is why the phone branch strips a country code purely on length\n * rather than re-checking the shape.\n *\n * @param key - Raw key, masked or not.\n * @returns The type and the normalised value.\n * @throws {PixError} When the key matches no accepted format, or when a document\n * key fails its check digits.\n *\n * @example\n * normalizePixKey(\"123.456.789-09\"); // { type: \"cpf\", value: \"12345678909\" }\n * normalizePixKey(\"(11) 98765-4321\"); // { type: \"phone\", value: \"+5511987654321\" }\n */\nexport function normalizePixKey(key: string): NormalizedPixKey {\n const trimmed = key.trim();\n const type = pixKeyType(trimmed);\n if (type === null) {\n throw new PixError(\n `Not a Pix key: ${JSON.stringify(key)}. Expected a CPF, CNPJ, e-mail, ` +\n \"phone (+5511987654321) or EVP (UUID).\",\n );\n }\n\n if (type === \"email\") return { type, value: trimmed.toLowerCase() };\n if (type === \"evp\") return { type, value: trimmed.toLowerCase() };\n if (type === \"phone\") {\n const numbers = digits(trimmed);\n return { type, value: `+55${numbers.length > 11 ? numbers.slice(2) : numbers}` };\n }\n return { type, value: digits(trimmed) };\n}\n\n/** Format an amount the way tag 54 wants it: dot separator, two decimals. */\nfunction toAmountField(amount: number): string {\n if (!Number.isFinite(amount)) {\n throw new PixError(`Amount must be a finite number, got ${amount}.`);\n }\n if (amount <= 0) {\n throw new PixError(\n `Amount must be positive, got ${amount}. Omit \\`amount\\` for a QR whose value the payer types.`,\n );\n }\n const field = amount.toFixed(2);\n if (field.length > 13) {\n throw new PixError(`Amount ${field} does not fit tag 54, which holds 13 characters.`);\n }\n return field;\n}\n\n/** Bound the two free-text identity fields the spec caps hard. */\nfunction toBoundedText(value: string, max: number, field: string): string {\n const text = toPayloadText(value.trim(), field);\n if (text === \"\") throw new PixError(`${field} is required.`);\n if (text.length > max) {\n throw new PixError(\n `${field} is ${text.length} characters; the BR Code allows ${max}. Shorten it — ` +\n \"truncating here would silently change what the payer sees.\",\n );\n }\n return text;\n}\n\n/** Tag 62, which exists only to carry the txid. */\nfunction additionalDataField(txid: string | undefined): string {\n const value = txid?.trim() ?? \"\";\n if (value === \"\" || value === TXID_UNSPECIFIED) {\n return tlv(TAG_ADDITIONAL_DATA, tlv(ADDITIONAL_TAG_TXID, TXID_UNSPECIFIED));\n }\n if (!new RegExp(`^[A-Za-z0-9]{1,${MAX_TXID}}$`).test(value)) {\n throw new PixError(\n `txid must be 1 to ${MAX_TXID} letters or digits, got ${JSON.stringify(txid)}.`,\n );\n }\n return tlv(TAG_ADDITIONAL_DATA, tlv(ADDITIONAL_TAG_TXID, value));\n}\n\n/** Tag 26 for a static payload: GUI, key and the optional description. */\nfunction staticMerchantAccount(input: PixStaticInput): string {\n const { value } = normalizePixKey(input.key);\n let inner = tlv(MAI_TAG_GUI, PIX_GUI) + tlv(MAI_TAG_KEY, value);\n const description = input.description?.trim();\n if (description !== undefined && description !== \"\") {\n inner += tlv(MAI_TAG_DESCRIPTION, toPayloadText(description, \"description\"));\n }\n if (inner.length > MAX_TLV_VALUE) {\n throw new PixError(\n `Tag 26 is ${inner.length} characters, over the ${MAX_TLV_VALUE} the length field allows. ` +\n \"Shorten `description`.\",\n );\n }\n return tlv(TAG_MERCHANT_ACCOUNT_INFO, inner);\n}\n\n/** Tag 26 for a dynamic payload: GUI plus the URL, and no key. */\nfunction dynamicMerchantAccount(input: PixDynamicInput): string {\n const url = toPayloadText(input.url.trim(), \"url\").replace(/^https?:\\/\\//i, \"\");\n if (url === \"\") throw new PixError(\"url is required for a dynamic BR Code.\");\n const inner = tlv(MAI_TAG_GUI, PIX_GUI) + tlv(MAI_TAG_URL, url);\n if (inner.length > MAX_TLV_VALUE) {\n throw new PixError(\n `Tag 26 is ${inner.length} characters, over the ${MAX_TLV_VALUE} the length field allows. ` +\n \"Shorten the payload URL.\",\n );\n }\n return tlv(TAG_MERCHANT_ACCOUNT_INFO, inner);\n}\n\n/**\n * Build a Pix \"Copia e Cola\" payload — the string behind a Pix QR code.\n *\n * The format is EMVCo MPM: a flat list of `ID + 2-digit length + value` triples,\n * closed by tag 63 holding a CRC-16/CCITT-FALSE over **everything before it,\n * including the literal `6304` header of tag 63 itself**. That last detail is the\n * one implementations get wrong; see {@link pixCrc16}.\n *\n * Two shapes come out of here:\n *\n * - **static** — tag 26 carries the key, so the QR is self-contained and can be\n * printed. Amount optional; a txid of `\"***\"` means \"identifies no single\n * transaction\", which is what a reusable poster QR wants.\n * - **dynamic** — tag 26 carries a URL instead, and the wallet fetches the amount\n * and payee from the PSP. Use it when the value is per-order. Defaults to\n * single-use (tag 01 = `12`).\n *\n * The distinction matters and is not cosmetic: a static QR settles against\n * whatever the payer typed, a dynamic one against what the PSP served, so a\n * charge that must reconcile to a cent needs the dynamic form.\n *\n * @param input - Static or dynamic payload description.\n * @returns The full payload, CRC included, ready to render as a QR or to copy.\n * @throws {PixError} On an unrecognised key, a field over its length cap, a\n * non-positive amount, or text that is not representable in the BR Code.\n *\n * @example\n * pixPayload({\n * key: \"12345678909\",\n * merchantName: \"Loja Tempest\",\n * merchantCity: \"São Paulo\",\n * amount: 25.5,\n * txid: \"PEDIDO123\",\n * });\n */\nexport function pixPayload(input: PixInput): string {\n const dynamic = input.kind === \"dynamic\";\n const oneTime = input.oneTime ?? dynamic;\n\n let payload = tlv(TAG_PAYLOAD_FORMAT, PAYLOAD_FORMAT_VERSION);\n payload += tlv(\n TAG_POINT_OF_INITIATION,\n oneTime ? POINT_OF_INITIATION_SINGLE_USE : POINT_OF_INITIATION_REUSABLE,\n );\n payload += dynamic ? dynamicMerchantAccount(input) : staticMerchantAccount(input);\n payload += tlv(TAG_MERCHANT_CATEGORY_CODE, DEFAULT_MERCHANT_CATEGORY_CODE);\n payload += tlv(TAG_TRANSACTION_CURRENCY, CURRENCY_BRL);\n if (!dynamic && input.amount !== undefined) {\n payload += tlv(TAG_TRANSACTION_AMOUNT, toAmountField(input.amount));\n }\n payload += tlv(TAG_COUNTRY_CODE, COUNTRY_BR);\n payload += tlv(\n TAG_MERCHANT_NAME,\n toBoundedText(input.merchantName, MAX_MERCHANT_NAME, \"merchantName\"),\n );\n payload += tlv(\n TAG_MERCHANT_CITY,\n toBoundedText(input.merchantCity, MAX_MERCHANT_CITY, \"merchantCity\"),\n );\n const postalCode = input.postalCode === undefined ? \"\" : digits(input.postalCode);\n if (postalCode !== \"\") payload += tlv(TAG_POSTAL_CODE, postalCode);\n payload += additionalDataField(dynamic ? TXID_UNSPECIFIED : input.txid);\n\n const withCrcHeader = `${payload}${TAG_CRC}04`;\n return `${withCrcHeader}${pixCrc16(withCrcHeader)}`;\n}\n\n/**\n * Split a flat run of TLVs. Stops cleanly at the first malformed triple.\n *\n * @throws {PixError} When a length prefix is not two digits or runs past the end.\n */\nfunction parseTlv(input: string, where: string): PixField[] {\n const fields: PixField[] = [];\n let cursor = 0;\n while (cursor < input.length) {\n const id = input.slice(cursor, cursor + 2);\n const rawLength = input.slice(cursor + 2, cursor + 4);\n if (!/^\\d{2}$/.test(id) || !/^\\d{2}$/.test(rawLength)) {\n throw new PixError(\n `Malformed TLV in ${where} at offset ${cursor}: expected a 2-digit tag and length.`,\n );\n }\n const length = Number(rawLength);\n const start = cursor + 4;\n if (start + length > input.length) {\n throw new PixError(\n `Tag ${id} in ${where} declares ${length} characters but only ${input.length - start} remain.`,\n );\n }\n fields.push({ id, value: input.slice(start, start + length) });\n cursor = start + length;\n }\n return fields;\n}\n\n/** First value for a tag, or `undefined`. */\nfunction pick(fields: readonly PixField[], id: string): string | undefined {\n return fields.find((field) => field.id === id)?.value;\n}\n\n/**\n * Read a Pix \"Copia e Cola\" payload back into its parts.\n *\n * Tolerant by design: tags the SDK does not know about are kept verbatim in\n * {@link PixData.fields} instead of raising, because PSPs do add their own\n * templates and a reader that rejects them is useless in production. What is\n * *not* tolerated is a broken frame — a length prefix that runs off the end, a\n * missing tag 63 — or a checksum mismatch, which means the string was corrupted\n * and no longer names the account the payee published.\n *\n * @param payload - The copia-e-cola string. Surrounding whitespace is ignored.\n * @param options - See {@link ParsePixOptions}.\n * @returns The decoded payload.\n * @throws {PixError} On a malformed frame, a missing CRC tag, a tag 26 that is\n * not a Pix account, or — unless `requireCrc` is `false` — a CRC mismatch.\n *\n * @example\n * const data = parsePixPayload(copied);\n * console.log(data.key, data.amount, data.txid);\n */\nexport function parsePixPayload(payload: string, options: ParsePixOptions = {}): PixData {\n const { requireCrc = true } = options;\n const text = payload.trim();\n if (text.length < 8) throw new PixError(\"Payload is too short to be a BR Code.\");\n\n const crcHeaderAt = text.length - 8;\n if (text.slice(crcHeaderAt, crcHeaderAt + 4) !== `${TAG_CRC}04`) {\n throw new PixError(\"Payload does not end in a `6304` CRC tag.\");\n }\n const crc = text.slice(-4).toUpperCase();\n const expected = pixCrc16(text.slice(0, -4));\n const crcValid = crc === expected;\n if (!crcValid && requireCrc) {\n throw new PixError(`CRC mismatch: payload says ${crc}, recomputed ${expected}.`);\n }\n\n const fields = parseTlv(text.slice(0, crcHeaderAt), \"payload\");\n const merchantAccount = pick(fields, TAG_MERCHANT_ACCOUNT_INFO);\n if (merchantAccount === undefined) {\n throw new PixError(\"Payload has no tag 26 (merchant account information).\");\n }\n const account = parseTlv(merchantAccount, \"tag 26\");\n const gui = pick(account, MAI_TAG_GUI);\n if (gui?.toLowerCase() !== PIX_GUI) {\n throw new PixError(\n `Tag 26 is not a Pix account: expected GUI ${PIX_GUI}, got ${JSON.stringify(gui)}.`,\n );\n }\n\n const url = pick(account, MAI_TAG_URL);\n const key = pick(account, MAI_TAG_KEY);\n const amountField = pick(fields, TAG_TRANSACTION_AMOUNT);\n const txid = pick(\n parseTlv(pick(fields, TAG_ADDITIONAL_DATA) ?? \"\", \"tag 62\"),\n ADDITIONAL_TAG_TXID,\n );\n\n return {\n kind: url !== undefined && key === undefined ? \"dynamic\" : \"static\",\n ...(key === undefined ? {} : { key, keyType: pixKeyType(key) ?? undefined }),\n ...(url === undefined ? {} : { url }),\n merchantName: pick(fields, TAG_MERCHANT_NAME) ?? \"\",\n merchantCity: pick(fields, TAG_MERCHANT_CITY) ?? \"\",\n ...(amountField === undefined ? {} : { amount: Number(amountField) }),\n currency: pick(fields, TAG_TRANSACTION_CURRENCY) ?? \"\",\n countryCode: pick(fields, TAG_COUNTRY_CODE) ?? \"\",\n merchantCategoryCode: pick(fields, TAG_MERCHANT_CATEGORY_CODE) ?? \"\",\n ...(txid === undefined ? {} : { txid }),\n ...(pick(account, MAI_TAG_DESCRIPTION) === undefined\n ? {}\n : { description: pick(account, MAI_TAG_DESCRIPTION) }),\n ...(pick(fields, TAG_POSTAL_CODE) === undefined\n ? {}\n : { postalCode: pick(fields, TAG_POSTAL_CODE) }),\n oneTime: pick(fields, TAG_POINT_OF_INITIATION) === POINT_OF_INITIATION_SINGLE_USE,\n crc,\n crcValid,\n fields,\n };\n}\n"],"mappings":"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"}
package/dist/br.d.ts CHANGED
@@ -124,6 +124,14 @@ export declare function boletoDueDate(fator: number, options?: BoletoOptions): {
124
124
  /** Which base date the fator de vencimento counts from. See {@link boletoDueDate}. */
125
125
  export declare type BoletoEpoch = "auto" | "legacy" | "current";
126
126
 
127
+ /**
128
+ * @tempest-limits file-lines — the FEBRABAN spec in one file: the 47-digit linha
129
+ * digitável, the 44-digit barcode, the two layouts (bank slips and arrecadação),
130
+ * modulo-10 and modulo-11 check digits, the base date the due date counts from, and
131
+ * the value scaling. Every piece cross-checks another — the conversion between the
132
+ * two forms is what proves the check digits — so splitting it hides the one property
133
+ * the file exists to guarantee.
134
+ */
127
135
  /**
128
136
  * A boleto string could not be read, or failed a check digit.
129
137
  *
@@ -1 +1 @@
1
- {"version":3,"file":"media-recorder.cjs","names":[],"sources":["../../src/capture/media-recorder.ts"],"sourcesContent":["/** Lifecycle of a recording. */\nexport type MediaRecorderStatus = \"idle\" | \"recording\" | \"paused\" | \"stopped\";\n\n/** What a track carries — used only to word the errors this engine throws. */\nexport type MediaRecordingKind = \"audio\" | \"video\";\n\n/** A finished recording, whatever it was made of. */\nexport interface MediaRecording {\n /** The bytes. 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 createMediaRecorder}. */\nexport interface MediaRecordingOptions {\n /**\n * Container candidates, best first. Required: this engine has no opinion about\n * codecs — the audio and video wrappers own that list.\n */\n candidates: readonly string[];\n /** Wording for the thrown messages (\"cannot record any supported *video* container\"). */\n kind: MediaRecordingKind;\n /** Force a container, bypassing the negotiation. Throws when unsupported. */\n mimeType?: string;\n /** Target audio bitrate. */\n audioBitsPerSecond?: number;\n /** Target video bitrate. Ignored by the browser on an audio-only stream. */\n videoBitsPerSecond?: 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()`.\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 over one `MediaStream`. */\nexport interface MediaRecorderHandle {\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<MediaRecording>;\n /** Stop and throw the bytes away. */\n cancel: () => void;\n status: () => MediaRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: () => number;\n /** The negotiated container. */\n mimeType: string;\n}\n\n/**\n * First container in `preferred` the browser can actually produce, or `null`.\n *\n * @param preferred - Candidates, best first.\n * @returns A supported MIME type, or `null` when none is.\n */\nexport function pickRecordingMimeType(preferred: readonly string[]): string | null {\n if (typeof MediaRecorder === \"undefined\") return null;\n // Older WebViews ship `MediaRecorder` without the static probe. Assume the first\n // candidate rather than refusing outright — the constructor will tell us.\n if (typeof MediaRecorder.isTypeSupported !== \"function\") return preferred[0] ?? null;\n return preferred.find((type) => MediaRecorder.isTypeSupported(type)) ?? null;\n}\n\n/**\n * Wrap a `MediaStream` in a recorder with a real state machine and an honest clock.\n *\n * This is the engine behind both `createAudioRecorder` and `createVideoRecorder`; the\n * only thing it does not decide is which containers to try, because that is the one\n * part that genuinely differs between the two.\n *\n * Two things `MediaRecorder` does not give you:\n *\n * - **A duration.** It reports none, and the `Blob` has no reliable one either —\n * WebM written by `MediaRecorder` carries no duration in its header, which is why\n * `<audio>`/`<video>` shows `Infinity` for a fresh recording. So the clock is kept\n * here, and it subtracts paused time: a recorder that counts wall-clock through a\n * pause reports a 30-second note as two minutes.\n * - **A promise from `stop()`.** The last chunk arrives *after* `stop()` returns, in\n * a `dataavailable` event that fires before `onstop`. Assembling the blob in\n * `onstop` is the only point where every chunk is in hand.\n *\n * The stream is **not** owned here: `stop()` leaves the device open so a retake does\n * not need a second permission round-trip. Release it with the owning hook's `stop()`.\n *\n * @param stream - A live stream, from `getUserMedia` or `getDisplayMedia`.\n * @param options - See {@link MediaRecordingOptions}.\n * @returns The imperative recorder.\n * @throws When `MediaRecorder` is unavailable, or an explicit `mimeType` is unsupported.\n */\nexport function createMediaRecorder(\n stream: MediaStream,\n options: MediaRecordingOptions,\n): MediaRecorderHandle {\n const {\n candidates,\n kind,\n mimeType: forced,\n audioBitsPerSecond,\n videoBitsPerSecond,\n timesliceMs,\n onChunk,\n onError,\n } = options;\n\n if (typeof MediaRecorder === \"undefined\") {\n throw new Error(\"MediaRecorder is not available in this environment.\");\n }\n const negotiated = forced ?? pickRecordingMimeType(candidates);\n if (negotiated === null) {\n throw new Error(`This browser cannot record any supported ${kind} container.`);\n }\n if (\n forced !== undefined &&\n typeof MediaRecorder.isTypeSupported === \"function\" &&\n !MediaRecorder.isTypeSupported(forced)\n ) {\n throw new Error(`This browser cannot record \"${forced}\".`);\n }\n\n const recorder = new MediaRecorder(stream, {\n mimeType: negotiated,\n ...(audioBitsPerSecond !== undefined ? { audioBitsPerSecond } : {}),\n ...(videoBitsPerSecond !== undefined ? { videoBitsPerSecond } : {}),\n });\n\n let chunks: Blob[] = [];\n let status: MediaRecorderStatus = \"idle\";\n let accumulatedMs = 0;\n let segmentStart = 0;\n let settle: ((recording: MediaRecording) => void) | null = null;\n let discard = false;\n\n const elapsed = (): number =>\n accumulatedMs + (status === \"recording\" ? Date.now() - segmentStart : 0);\n\n recorder.ondataavailable = (event: BlobEvent): void => {\n if (event.data.size === 0) return;\n if (discard) return;\n chunks.push(event.data);\n onChunk?.(event.data);\n };\n\n recorder.onerror = (event: Event): void => {\n onError?.((event as unknown as { error?: unknown }).error ?? event);\n };\n\n recorder.onstop = (): void => {\n const durationMs = elapsed();\n status = \"stopped\";\n const resolve = settle;\n settle = null;\n if (discard) {\n chunks = [];\n return;\n }\n // `recorder.mimeType` is the source of truth: a browser handed\n // `video/webm;codecs=vp9,opus` may report plain `video/webm` back.\n const type = recorder.mimeType || negotiated;\n resolve?.({ blob: new Blob(chunks, { type }), mimeType: type, durationMs });\n chunks = [];\n };\n\n return {\n mimeType: negotiated,\n status: () => status,\n durationMs: elapsed,\n\n start(): void {\n if (status === \"recording\" || status === \"paused\") return;\n chunks = [];\n accumulatedMs = 0;\n discard = false;\n segmentStart = Date.now();\n status = \"recording\";\n if (timesliceMs !== undefined) recorder.start(timesliceMs);\n else recorder.start();\n },\n\n pause(): void {\n if (status !== \"recording\") return;\n accumulatedMs += Date.now() - segmentStart;\n status = \"paused\";\n recorder.pause();\n },\n\n resume(): void {\n if (status !== \"paused\") return;\n segmentStart = Date.now();\n status = \"recording\";\n recorder.resume();\n },\n\n stop(): Promise<MediaRecording> {\n if (status === \"idle\" || status === \"stopped\") {\n return Promise.resolve({\n blob: new Blob([], { type: negotiated }),\n mimeType: negotiated,\n durationMs: 0,\n });\n }\n // Stopping while paused needs no clock fix-up: `pause()` already folded\n // the last segment into `accumulatedMs`, and `elapsed()` adds nothing\n // while the status is not `\"recording\"`.\n return new Promise<MediaRecording>((resolve) => {\n settle = resolve;\n recorder.stop();\n });\n },\n\n cancel(): void {\n if (status === \"idle\" || status === \"stopped\") return;\n discard = true;\n settle = null;\n recorder.stop();\n },\n };\n}\n"],"mappings":"AAoEA,SAAgB,EAAsB,EAA6C,CAK/E,OAJI,OAAO,cAAkB,IAAoB,KAG7C,OAAO,cAAc,iBAAoB,WACtC,EAAU,KAAM,GAAS,cAAc,gBAAgB,CAAI,CAAC,GAAK,KADR,EAAU,IAAM,IAEpF,CA4BA,SAAgB,EACZ,EACA,EACmB,CACnB,GAAM,CACF,aACA,OACA,SAAU,EACV,qBACA,qBACA,cACA,UACA,WACA,EAEJ,GAAI,OAAO,cAAkB,IACzB,MAAU,MAAM,qDAAqD,EAEzE,IAAM,EAAa,GAAU,EAAsB,CAAU,EAC7D,GAAI,IAAe,KACf,MAAU,MAAM,4CAA4C,EAAK,YAAY,EAEjF,GACI,IAAW,IAAA,IACX,OAAO,cAAc,iBAAoB,YACzC,CAAC,cAAc,gBAAgB,CAAM,EAErC,MAAU,MAAM,+BAA+B,EAAO,GAAG,EAG7D,IAAM,EAAW,IAAI,cAAc,EAAQ,CACvC,SAAU,EACV,GAAI,IAAuB,IAAA,GAAqC,CAAC,EAA1B,CAAE,oBAAmB,EAC5D,GAAI,IAAuB,IAAA,GAAqC,CAAC,EAA1B,CAAE,oBAAmB,CAChE,CAAC,EAEG,EAAiB,CAAC,EAClB,EAA8B,OAC9B,EAAgB,EAChB,EAAe,EACf,EAAuD,KACvD,EAAU,GAER,MACF,GAAiB,IAAW,YAAc,KAAK,IAAI,EAAI,EAAe,GA6B1E,MA3BA,GAAS,gBAAmB,GAA2B,CAC/C,EAAM,KAAK,OAAS,IACpB,IACJ,EAAO,KAAK,EAAM,IAAI,EACtB,IAAU,EAAM,IAAI,GACxB,EAEA,EAAS,QAAW,GAAuB,CACvC,IAAW,EAAyC,OAAS,CAAK,CACtE,EAEA,EAAS,WAAqB,CAC1B,IAAM,EAAa,EAAQ,EAC3B,EAAS,UACT,IAAM,EAAU,EAEhB,GADA,EAAS,KACL,EAAS,CACT,EAAS,CAAC,EACV,MACJ,CAGA,IAAM,EAAO,EAAS,UAAY,EAClC,IAAU,CAAE,KAAM,IAAI,KAAK,EAAQ,CAAE,MAAK,CAAC,EAAG,SAAU,EAAM,YAAW,CAAC,EAC1E,EAAS,CAAC,CACd,EAEO,CACH,SAAU,EACV,WAAc,EACd,WAAY,EAEZ,OAAc,CACN,IAAW,aAAe,IAAW,WACzC,EAAS,CAAC,EACV,EAAgB,EAChB,EAAU,GACV,EAAe,KAAK,IAAI,EACxB,EAAS,YACL,IAAgB,IAAA,GACf,EAAS,MAAM,EADW,EAAS,MAAM,CAAW,EAE7D,EAEA,OAAc,CACN,IAAW,cACf,GAAiB,KAAK,IAAI,EAAI,EAC9B,EAAS,SACT,EAAS,MAAM,EACnB,EAEA,QAAe,CACP,IAAW,WACf,EAAe,KAAK,IAAI,EACxB,EAAS,YACT,EAAS,OAAO,EACpB,EAEA,MAAgC,CAW5B,OAVI,IAAW,QAAU,IAAW,UACzB,QAAQ,QAAQ,CACnB,KAAM,IAAI,KAAK,CAAC,EAAG,CAAE,KAAM,CAAW,CAAC,EACvC,SAAU,EACV,WAAY,CAChB,CAAC,EAKE,IAAI,QAAyB,GAAY,CAC5C,EAAS,EACT,EAAS,KAAK,CAClB,CAAC,CACL,EAEA,QAAe,CACP,IAAW,QAAU,IAAW,YACpC,EAAU,GACV,EAAS,KACT,EAAS,KAAK,EAClB,CACJ,CACJ"}
1
+ {"version":3,"file":"media-recorder.cjs","names":[],"sources":["../../src/capture/media-recorder.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — the engine behind both the audio and the video\n * recorder: MIME negotiation, the state machine MediaRecorder does not give you, and\n * the clock kept by hand because a fresh WebM reports no duration. The clock has to\n * pause and resume with the state machine, so they are one closure.\n */\n/** Lifecycle of a recording. */\nexport type MediaRecorderStatus = \"idle\" | \"recording\" | \"paused\" | \"stopped\";\n\n/** What a track carries — used only to word the errors this engine throws. */\nexport type MediaRecordingKind = \"audio\" | \"video\";\n\n/** A finished recording, whatever it was made of. */\nexport interface MediaRecording {\n /** The bytes. 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 createMediaRecorder}. */\nexport interface MediaRecordingOptions {\n /**\n * Container candidates, best first. Required: this engine has no opinion about\n * codecs — the audio and video wrappers own that list.\n */\n candidates: readonly string[];\n /** Wording for the thrown messages (\"cannot record any supported *video* container\"). */\n kind: MediaRecordingKind;\n /** Force a container, bypassing the negotiation. Throws when unsupported. */\n mimeType?: string;\n /** Target audio bitrate. */\n audioBitsPerSecond?: number;\n /** Target video bitrate. Ignored by the browser on an audio-only stream. */\n videoBitsPerSecond?: 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()`.\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 over one `MediaStream`. */\nexport interface MediaRecorderHandle {\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<MediaRecording>;\n /** Stop and throw the bytes away. */\n cancel: () => void;\n status: () => MediaRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: () => number;\n /** The negotiated container. */\n mimeType: string;\n}\n\n/**\n * First container in `preferred` the browser can actually produce, or `null`.\n *\n * @param preferred - Candidates, best first.\n * @returns A supported MIME type, or `null` when none is.\n */\nexport function pickRecordingMimeType(preferred: readonly string[]): string | null {\n if (typeof MediaRecorder === \"undefined\") return null;\n // Older WebViews ship `MediaRecorder` without the static probe. Assume the first\n // candidate rather than refusing outright — the constructor will tell us.\n if (typeof MediaRecorder.isTypeSupported !== \"function\") return preferred[0] ?? null;\n return preferred.find((type) => MediaRecorder.isTypeSupported(type)) ?? null;\n}\n\n/**\n * Wrap a `MediaStream` in a recorder with a real state machine and an honest clock.\n *\n * This is the engine behind both `createAudioRecorder` and `createVideoRecorder`; the\n * only thing it does not decide is which containers to try, because that is the one\n * part that genuinely differs between the two.\n *\n * Two things `MediaRecorder` does not give you:\n *\n * - **A duration.** It reports none, and the `Blob` has no reliable one either —\n * WebM written by `MediaRecorder` carries no duration in its header, which is why\n * `<audio>`/`<video>` shows `Infinity` for a fresh recording. So the clock is kept\n * here, and it subtracts paused time: a recorder that counts wall-clock through a\n * pause reports a 30-second note as two minutes.\n * - **A promise from `stop()`.** The last chunk arrives *after* `stop()` returns, in\n * a `dataavailable` event that fires before `onstop`. Assembling the blob in\n * `onstop` is the only point where every chunk is in hand.\n *\n * The stream is **not** owned here: `stop()` leaves the device open so a retake does\n * not need a second permission round-trip. Release it with the owning hook's `stop()`.\n *\n * @param stream - A live stream, from `getUserMedia` or `getDisplayMedia`.\n * @param options - See {@link MediaRecordingOptions}.\n * @returns The imperative recorder.\n * @throws When `MediaRecorder` is unavailable, or an explicit `mimeType` is unsupported.\n */\nexport function createMediaRecorder(\n stream: MediaStream,\n options: MediaRecordingOptions,\n): MediaRecorderHandle {\n const {\n candidates,\n kind,\n mimeType: forced,\n audioBitsPerSecond,\n videoBitsPerSecond,\n timesliceMs,\n onChunk,\n onError,\n } = options;\n\n if (typeof MediaRecorder === \"undefined\") {\n throw new Error(\"MediaRecorder is not available in this environment.\");\n }\n const negotiated = forced ?? pickRecordingMimeType(candidates);\n if (negotiated === null) {\n throw new Error(`This browser cannot record any supported ${kind} container.`);\n }\n if (\n forced !== undefined &&\n typeof MediaRecorder.isTypeSupported === \"function\" &&\n !MediaRecorder.isTypeSupported(forced)\n ) {\n throw new Error(`This browser cannot record \"${forced}\".`);\n }\n\n const recorder = new MediaRecorder(stream, {\n mimeType: negotiated,\n ...(audioBitsPerSecond !== undefined ? { audioBitsPerSecond } : {}),\n ...(videoBitsPerSecond !== undefined ? { videoBitsPerSecond } : {}),\n });\n\n let chunks: Blob[] = [];\n let status: MediaRecorderStatus = \"idle\";\n let accumulatedMs = 0;\n let segmentStart = 0;\n let settle: ((recording: MediaRecording) => void) | null = null;\n let discard = false;\n\n const elapsed = (): number =>\n accumulatedMs + (status === \"recording\" ? Date.now() - segmentStart : 0);\n\n recorder.ondataavailable = (event: BlobEvent): void => {\n if (event.data.size === 0) return;\n if (discard) return;\n chunks.push(event.data);\n onChunk?.(event.data);\n };\n\n recorder.onerror = (event: Event): void => {\n onError?.((event as unknown as { error?: unknown }).error ?? event);\n };\n\n recorder.onstop = (): void => {\n const durationMs = elapsed();\n status = \"stopped\";\n const resolve = settle;\n settle = null;\n if (discard) {\n chunks = [];\n return;\n }\n // `recorder.mimeType` is the source of truth: a browser handed\n // `video/webm;codecs=vp9,opus` may report plain `video/webm` back.\n const type = recorder.mimeType || negotiated;\n resolve?.({ blob: new Blob(chunks, { type }), mimeType: type, durationMs });\n chunks = [];\n };\n\n return {\n mimeType: negotiated,\n status: () => status,\n durationMs: elapsed,\n\n start(): void {\n if (status === \"recording\" || status === \"paused\") return;\n chunks = [];\n accumulatedMs = 0;\n discard = false;\n segmentStart = Date.now();\n status = \"recording\";\n if (timesliceMs !== undefined) recorder.start(timesliceMs);\n else recorder.start();\n },\n\n pause(): void {\n if (status !== \"recording\") return;\n accumulatedMs += Date.now() - segmentStart;\n status = \"paused\";\n recorder.pause();\n },\n\n resume(): void {\n if (status !== \"paused\") return;\n segmentStart = Date.now();\n status = \"recording\";\n recorder.resume();\n },\n\n stop(): Promise<MediaRecording> {\n if (status === \"idle\" || status === \"stopped\") {\n return Promise.resolve({\n blob: new Blob([], { type: negotiated }),\n mimeType: negotiated,\n durationMs: 0,\n });\n }\n // Stopping while paused needs no clock fix-up: `pause()` already folded\n // the last segment into `accumulatedMs`, and `elapsed()` adds nothing\n // while the status is not `\"recording\"`.\n return new Promise<MediaRecording>((resolve) => {\n settle = resolve;\n recorder.stop();\n });\n },\n\n cancel(): void {\n if (status === \"idle\" || status === \"stopped\") return;\n discard = true;\n settle = null;\n recorder.stop();\n },\n };\n}\n"],"mappings":"AA0EA,SAAgB,EAAsB,EAA6C,CAK/E,OAJI,OAAO,cAAkB,IAAoB,KAG7C,OAAO,cAAc,iBAAoB,WACtC,EAAU,KAAM,GAAS,cAAc,gBAAgB,CAAI,CAAC,GAAK,KADR,EAAU,IAAM,IAEpF,CA4BA,SAAgB,EACZ,EACA,EACmB,CACnB,GAAM,CACF,aACA,OACA,SAAU,EACV,qBACA,qBACA,cACA,UACA,WACA,EAEJ,GAAI,OAAO,cAAkB,IACzB,MAAU,MAAM,qDAAqD,EAEzE,IAAM,EAAa,GAAU,EAAsB,CAAU,EAC7D,GAAI,IAAe,KACf,MAAU,MAAM,4CAA4C,EAAK,YAAY,EAEjF,GACI,IAAW,IAAA,IACX,OAAO,cAAc,iBAAoB,YACzC,CAAC,cAAc,gBAAgB,CAAM,EAErC,MAAU,MAAM,+BAA+B,EAAO,GAAG,EAG7D,IAAM,EAAW,IAAI,cAAc,EAAQ,CACvC,SAAU,EACV,GAAI,IAAuB,IAAA,GAAqC,CAAC,EAA1B,CAAE,oBAAmB,EAC5D,GAAI,IAAuB,IAAA,GAAqC,CAAC,EAA1B,CAAE,oBAAmB,CAChE,CAAC,EAEG,EAAiB,CAAC,EAClB,EAA8B,OAC9B,EAAgB,EAChB,EAAe,EACf,EAAuD,KACvD,EAAU,GAER,MACF,GAAiB,IAAW,YAAc,KAAK,IAAI,EAAI,EAAe,GA6B1E,MA3BA,GAAS,gBAAmB,GAA2B,CAC/C,EAAM,KAAK,OAAS,IACpB,IACJ,EAAO,KAAK,EAAM,IAAI,EACtB,IAAU,EAAM,IAAI,GACxB,EAEA,EAAS,QAAW,GAAuB,CACvC,IAAW,EAAyC,OAAS,CAAK,CACtE,EAEA,EAAS,WAAqB,CAC1B,IAAM,EAAa,EAAQ,EAC3B,EAAS,UACT,IAAM,EAAU,EAEhB,GADA,EAAS,KACL,EAAS,CACT,EAAS,CAAC,EACV,MACJ,CAGA,IAAM,EAAO,EAAS,UAAY,EAClC,IAAU,CAAE,KAAM,IAAI,KAAK,EAAQ,CAAE,MAAK,CAAC,EAAG,SAAU,EAAM,YAAW,CAAC,EAC1E,EAAS,CAAC,CACd,EAEO,CACH,SAAU,EACV,WAAc,EACd,WAAY,EAEZ,OAAc,CACN,IAAW,aAAe,IAAW,WACzC,EAAS,CAAC,EACV,EAAgB,EAChB,EAAU,GACV,EAAe,KAAK,IAAI,EACxB,EAAS,YACL,IAAgB,IAAA,GACf,EAAS,MAAM,EADW,EAAS,MAAM,CAAW,EAE7D,EAEA,OAAc,CACN,IAAW,cACf,GAAiB,KAAK,IAAI,EAAI,EAC9B,EAAS,SACT,EAAS,MAAM,EACnB,EAEA,QAAe,CACP,IAAW,WACf,EAAe,KAAK,IAAI,EACxB,EAAS,YACT,EAAS,OAAO,EACpB,EAEA,MAAgC,CAW5B,OAVI,IAAW,QAAU,IAAW,UACzB,QAAQ,QAAQ,CACnB,KAAM,IAAI,KAAK,CAAC,EAAG,CAAE,KAAM,CAAW,CAAC,EACvC,SAAU,EACV,WAAY,CAChB,CAAC,EAKE,IAAI,QAAyB,GAAY,CAC5C,EAAS,EACT,EAAS,KAAK,CAClB,CAAC,CACL,EAEA,QAAe,CACP,IAAW,QAAU,IAAW,YACpC,EAAU,GACV,EAAS,KACT,EAAS,KAAK,EAClB,CACJ,CACJ"}
@@ -1 +1 @@
1
- {"version":3,"file":"media-recorder.js","names":[],"sources":["../../src/capture/media-recorder.ts"],"sourcesContent":["/** Lifecycle of a recording. */\nexport type MediaRecorderStatus = \"idle\" | \"recording\" | \"paused\" | \"stopped\";\n\n/** What a track carries — used only to word the errors this engine throws. */\nexport type MediaRecordingKind = \"audio\" | \"video\";\n\n/** A finished recording, whatever it was made of. */\nexport interface MediaRecording {\n /** The bytes. 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 createMediaRecorder}. */\nexport interface MediaRecordingOptions {\n /**\n * Container candidates, best first. Required: this engine has no opinion about\n * codecs — the audio and video wrappers own that list.\n */\n candidates: readonly string[];\n /** Wording for the thrown messages (\"cannot record any supported *video* container\"). */\n kind: MediaRecordingKind;\n /** Force a container, bypassing the negotiation. Throws when unsupported. */\n mimeType?: string;\n /** Target audio bitrate. */\n audioBitsPerSecond?: number;\n /** Target video bitrate. Ignored by the browser on an audio-only stream. */\n videoBitsPerSecond?: 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()`.\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 over one `MediaStream`. */\nexport interface MediaRecorderHandle {\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<MediaRecording>;\n /** Stop and throw the bytes away. */\n cancel: () => void;\n status: () => MediaRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: () => number;\n /** The negotiated container. */\n mimeType: string;\n}\n\n/**\n * First container in `preferred` the browser can actually produce, or `null`.\n *\n * @param preferred - Candidates, best first.\n * @returns A supported MIME type, or `null` when none is.\n */\nexport function pickRecordingMimeType(preferred: readonly string[]): string | null {\n if (typeof MediaRecorder === \"undefined\") return null;\n // Older WebViews ship `MediaRecorder` without the static probe. Assume the first\n // candidate rather than refusing outright — the constructor will tell us.\n if (typeof MediaRecorder.isTypeSupported !== \"function\") return preferred[0] ?? null;\n return preferred.find((type) => MediaRecorder.isTypeSupported(type)) ?? null;\n}\n\n/**\n * Wrap a `MediaStream` in a recorder with a real state machine and an honest clock.\n *\n * This is the engine behind both `createAudioRecorder` and `createVideoRecorder`; the\n * only thing it does not decide is which containers to try, because that is the one\n * part that genuinely differs between the two.\n *\n * Two things `MediaRecorder` does not give you:\n *\n * - **A duration.** It reports none, and the `Blob` has no reliable one either —\n * WebM written by `MediaRecorder` carries no duration in its header, which is why\n * `<audio>`/`<video>` shows `Infinity` for a fresh recording. So the clock is kept\n * here, and it subtracts paused time: a recorder that counts wall-clock through a\n * pause reports a 30-second note as two minutes.\n * - **A promise from `stop()`.** The last chunk arrives *after* `stop()` returns, in\n * a `dataavailable` event that fires before `onstop`. Assembling the blob in\n * `onstop` is the only point where every chunk is in hand.\n *\n * The stream is **not** owned here: `stop()` leaves the device open so a retake does\n * not need a second permission round-trip. Release it with the owning hook's `stop()`.\n *\n * @param stream - A live stream, from `getUserMedia` or `getDisplayMedia`.\n * @param options - See {@link MediaRecordingOptions}.\n * @returns The imperative recorder.\n * @throws When `MediaRecorder` is unavailable, or an explicit `mimeType` is unsupported.\n */\nexport function createMediaRecorder(\n stream: MediaStream,\n options: MediaRecordingOptions,\n): MediaRecorderHandle {\n const {\n candidates,\n kind,\n mimeType: forced,\n audioBitsPerSecond,\n videoBitsPerSecond,\n timesliceMs,\n onChunk,\n onError,\n } = options;\n\n if (typeof MediaRecorder === \"undefined\") {\n throw new Error(\"MediaRecorder is not available in this environment.\");\n }\n const negotiated = forced ?? pickRecordingMimeType(candidates);\n if (negotiated === null) {\n throw new Error(`This browser cannot record any supported ${kind} container.`);\n }\n if (\n forced !== undefined &&\n typeof MediaRecorder.isTypeSupported === \"function\" &&\n !MediaRecorder.isTypeSupported(forced)\n ) {\n throw new Error(`This browser cannot record \"${forced}\".`);\n }\n\n const recorder = new MediaRecorder(stream, {\n mimeType: negotiated,\n ...(audioBitsPerSecond !== undefined ? { audioBitsPerSecond } : {}),\n ...(videoBitsPerSecond !== undefined ? { videoBitsPerSecond } : {}),\n });\n\n let chunks: Blob[] = [];\n let status: MediaRecorderStatus = \"idle\";\n let accumulatedMs = 0;\n let segmentStart = 0;\n let settle: ((recording: MediaRecording) => void) | null = null;\n let discard = false;\n\n const elapsed = (): number =>\n accumulatedMs + (status === \"recording\" ? Date.now() - segmentStart : 0);\n\n recorder.ondataavailable = (event: BlobEvent): void => {\n if (event.data.size === 0) return;\n if (discard) return;\n chunks.push(event.data);\n onChunk?.(event.data);\n };\n\n recorder.onerror = (event: Event): void => {\n onError?.((event as unknown as { error?: unknown }).error ?? event);\n };\n\n recorder.onstop = (): void => {\n const durationMs = elapsed();\n status = \"stopped\";\n const resolve = settle;\n settle = null;\n if (discard) {\n chunks = [];\n return;\n }\n // `recorder.mimeType` is the source of truth: a browser handed\n // `video/webm;codecs=vp9,opus` may report plain `video/webm` back.\n const type = recorder.mimeType || negotiated;\n resolve?.({ blob: new Blob(chunks, { type }), mimeType: type, durationMs });\n chunks = [];\n };\n\n return {\n mimeType: negotiated,\n status: () => status,\n durationMs: elapsed,\n\n start(): void {\n if (status === \"recording\" || status === \"paused\") return;\n chunks = [];\n accumulatedMs = 0;\n discard = false;\n segmentStart = Date.now();\n status = \"recording\";\n if (timesliceMs !== undefined) recorder.start(timesliceMs);\n else recorder.start();\n },\n\n pause(): void {\n if (status !== \"recording\") return;\n accumulatedMs += Date.now() - segmentStart;\n status = \"paused\";\n recorder.pause();\n },\n\n resume(): void {\n if (status !== \"paused\") return;\n segmentStart = Date.now();\n status = \"recording\";\n recorder.resume();\n },\n\n stop(): Promise<MediaRecording> {\n if (status === \"idle\" || status === \"stopped\") {\n return Promise.resolve({\n blob: new Blob([], { type: negotiated }),\n mimeType: negotiated,\n durationMs: 0,\n });\n }\n // Stopping while paused needs no clock fix-up: `pause()` already folded\n // the last segment into `accumulatedMs`, and `elapsed()` adds nothing\n // while the status is not `\"recording\"`.\n return new Promise<MediaRecording>((resolve) => {\n settle = resolve;\n recorder.stop();\n });\n },\n\n cancel(): void {\n if (status === \"idle\" || status === \"stopped\") return;\n discard = true;\n settle = null;\n recorder.stop();\n },\n };\n}\n"],"mappings":";AAoEA,SAAgB,EAAsB,GAA6C;CAK/E,OAJI,OAAO,gBAAkB,MAAoB,OAG7C,OAAO,cAAc,mBAAoB,aACtC,EAAU,MAAM,MAAS,cAAc,gBAAgB,CAAI,CAAC,KAAK,OADR,EAAU,MAAM;AAEpF;AA4BA,SAAgB,EACZ,GACA,GACmB;CACnB,IAAM,EACF,eACA,SACA,UAAU,GACV,uBACA,uBACA,gBACA,YACA,eACA;CAEJ,IAAI,OAAO,gBAAkB,KACzB,MAAU,MAAM,qDAAqD;CAEzE,IAAM,IAAa,KAAU,EAAsB,CAAU;CAC7D,IAAI,MAAe,MACf,MAAU,MAAM,4CAA4C,EAAK,YAAY;CAEjF,IACI,MAAW,KAAA,KACX,OAAO,cAAc,mBAAoB,cACzC,CAAC,cAAc,gBAAgB,CAAM,GAErC,MAAU,MAAM,+BAA+B,EAAO,GAAG;CAG7D,IAAM,IAAW,IAAI,cAAc,GAAQ;EACvC,UAAU;EACV,GAAI,MAAuB,KAAA,IAAqC,CAAC,IAA1B,EAAE,sBAAmB;EAC5D,GAAI,MAAuB,KAAA,IAAqC,CAAC,IAA1B,EAAE,sBAAmB;CAChE,CAAC,GAEG,IAAiB,CAAC,GAClB,IAA8B,QAC9B,IAAgB,GAChB,IAAe,GACf,IAAuD,MACvD,IAAU,IAER,UACF,KAAiB,MAAW,cAAc,KAAK,IAAI,IAAI,IAAe;CA6B1E,OA3BA,EAAS,mBAAmB,MAA2B;EAC/C,EAAM,KAAK,SAAS,MACpB,MACJ,EAAO,KAAK,EAAM,IAAI,GACtB,IAAU,EAAM,IAAI;CACxB,GAEA,EAAS,WAAW,MAAuB;EACvC,IAAW,EAAyC,SAAS,CAAK;CACtE,GAEA,EAAS,eAAqB;EAC1B,IAAM,IAAa,EAAQ;EAC3B,IAAS;EACT,IAAM,IAAU;EAEhB,IADA,IAAS,MACL,GAAS;GACT,IAAS,CAAC;GACV;EACJ;EAGA,IAAM,IAAO,EAAS,YAAY;EAElC,AADA,IAAU;GAAE,MAAM,IAAI,KAAK,GAAQ,EAAE,QAAK,CAAC;GAAG,UAAU;GAAM;EAAW,CAAC,GAC1E,IAAS,CAAC;CACd,GAEO;EACH,UAAU;EACV,cAAc;EACd,YAAY;EAEZ,QAAc;GACN,MAAW,eAAe,MAAW,aACzC,IAAS,CAAC,GACV,IAAgB,GAChB,IAAU,IACV,IAAe,KAAK,IAAI,GACxB,IAAS,aACL,MAAgB,KAAA,IACf,EAAS,MAAM,IADW,EAAS,MAAM,CAAW;EAE7D;EAEA,QAAc;GACN,MAAW,gBACf,KAAiB,KAAK,IAAI,IAAI,GAC9B,IAAS,UACT,EAAS,MAAM;EACnB;EAEA,SAAe;GACP,MAAW,aACf,IAAe,KAAK,IAAI,GACxB,IAAS,aACT,EAAS,OAAO;EACpB;EAEA,OAAgC;GAW5B,OAVI,MAAW,UAAU,MAAW,YACzB,QAAQ,QAAQ;IACnB,MAAM,IAAI,KAAK,CAAC,GAAG,EAAE,MAAM,EAAW,CAAC;IACvC,UAAU;IACV,YAAY;GAChB,CAAC,IAKE,IAAI,SAAyB,MAAY;IAE5C,AADA,IAAS,GACT,EAAS,KAAK;GAClB,CAAC;EACL;EAEA,SAAe;GACP,MAAW,UAAU,MAAW,cACpC,IAAU,IACV,IAAS,MACT,EAAS,KAAK;EAClB;CACJ;AACJ"}
1
+ {"version":3,"file":"media-recorder.js","names":[],"sources":["../../src/capture/media-recorder.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — the engine behind both the audio and the video\n * recorder: MIME negotiation, the state machine MediaRecorder does not give you, and\n * the clock kept by hand because a fresh WebM reports no duration. The clock has to\n * pause and resume with the state machine, so they are one closure.\n */\n/** Lifecycle of a recording. */\nexport type MediaRecorderStatus = \"idle\" | \"recording\" | \"paused\" | \"stopped\";\n\n/** What a track carries — used only to word the errors this engine throws. */\nexport type MediaRecordingKind = \"audio\" | \"video\";\n\n/** A finished recording, whatever it was made of. */\nexport interface MediaRecording {\n /** The bytes. 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 createMediaRecorder}. */\nexport interface MediaRecordingOptions {\n /**\n * Container candidates, best first. Required: this engine has no opinion about\n * codecs — the audio and video wrappers own that list.\n */\n candidates: readonly string[];\n /** Wording for the thrown messages (\"cannot record any supported *video* container\"). */\n kind: MediaRecordingKind;\n /** Force a container, bypassing the negotiation. Throws when unsupported. */\n mimeType?: string;\n /** Target audio bitrate. */\n audioBitsPerSecond?: number;\n /** Target video bitrate. Ignored by the browser on an audio-only stream. */\n videoBitsPerSecond?: 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()`.\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 over one `MediaStream`. */\nexport interface MediaRecorderHandle {\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<MediaRecording>;\n /** Stop and throw the bytes away. */\n cancel: () => void;\n status: () => MediaRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: () => number;\n /** The negotiated container. */\n mimeType: string;\n}\n\n/**\n * First container in `preferred` the browser can actually produce, or `null`.\n *\n * @param preferred - Candidates, best first.\n * @returns A supported MIME type, or `null` when none is.\n */\nexport function pickRecordingMimeType(preferred: readonly string[]): string | null {\n if (typeof MediaRecorder === \"undefined\") return null;\n // Older WebViews ship `MediaRecorder` without the static probe. Assume the first\n // candidate rather than refusing outright — the constructor will tell us.\n if (typeof MediaRecorder.isTypeSupported !== \"function\") return preferred[0] ?? null;\n return preferred.find((type) => MediaRecorder.isTypeSupported(type)) ?? null;\n}\n\n/**\n * Wrap a `MediaStream` in a recorder with a real state machine and an honest clock.\n *\n * This is the engine behind both `createAudioRecorder` and `createVideoRecorder`; the\n * only thing it does not decide is which containers to try, because that is the one\n * part that genuinely differs between the two.\n *\n * Two things `MediaRecorder` does not give you:\n *\n * - **A duration.** It reports none, and the `Blob` has no reliable one either —\n * WebM written by `MediaRecorder` carries no duration in its header, which is why\n * `<audio>`/`<video>` shows `Infinity` for a fresh recording. So the clock is kept\n * here, and it subtracts paused time: a recorder that counts wall-clock through a\n * pause reports a 30-second note as two minutes.\n * - **A promise from `stop()`.** The last chunk arrives *after* `stop()` returns, in\n * a `dataavailable` event that fires before `onstop`. Assembling the blob in\n * `onstop` is the only point where every chunk is in hand.\n *\n * The stream is **not** owned here: `stop()` leaves the device open so a retake does\n * not need a second permission round-trip. Release it with the owning hook's `stop()`.\n *\n * @param stream - A live stream, from `getUserMedia` or `getDisplayMedia`.\n * @param options - See {@link MediaRecordingOptions}.\n * @returns The imperative recorder.\n * @throws When `MediaRecorder` is unavailable, or an explicit `mimeType` is unsupported.\n */\nexport function createMediaRecorder(\n stream: MediaStream,\n options: MediaRecordingOptions,\n): MediaRecorderHandle {\n const {\n candidates,\n kind,\n mimeType: forced,\n audioBitsPerSecond,\n videoBitsPerSecond,\n timesliceMs,\n onChunk,\n onError,\n } = options;\n\n if (typeof MediaRecorder === \"undefined\") {\n throw new Error(\"MediaRecorder is not available in this environment.\");\n }\n const negotiated = forced ?? pickRecordingMimeType(candidates);\n if (negotiated === null) {\n throw new Error(`This browser cannot record any supported ${kind} container.`);\n }\n if (\n forced !== undefined &&\n typeof MediaRecorder.isTypeSupported === \"function\" &&\n !MediaRecorder.isTypeSupported(forced)\n ) {\n throw new Error(`This browser cannot record \"${forced}\".`);\n }\n\n const recorder = new MediaRecorder(stream, {\n mimeType: negotiated,\n ...(audioBitsPerSecond !== undefined ? { audioBitsPerSecond } : {}),\n ...(videoBitsPerSecond !== undefined ? { videoBitsPerSecond } : {}),\n });\n\n let chunks: Blob[] = [];\n let status: MediaRecorderStatus = \"idle\";\n let accumulatedMs = 0;\n let segmentStart = 0;\n let settle: ((recording: MediaRecording) => void) | null = null;\n let discard = false;\n\n const elapsed = (): number =>\n accumulatedMs + (status === \"recording\" ? Date.now() - segmentStart : 0);\n\n recorder.ondataavailable = (event: BlobEvent): void => {\n if (event.data.size === 0) return;\n if (discard) return;\n chunks.push(event.data);\n onChunk?.(event.data);\n };\n\n recorder.onerror = (event: Event): void => {\n onError?.((event as unknown as { error?: unknown }).error ?? event);\n };\n\n recorder.onstop = (): void => {\n const durationMs = elapsed();\n status = \"stopped\";\n const resolve = settle;\n settle = null;\n if (discard) {\n chunks = [];\n return;\n }\n // `recorder.mimeType` is the source of truth: a browser handed\n // `video/webm;codecs=vp9,opus` may report plain `video/webm` back.\n const type = recorder.mimeType || negotiated;\n resolve?.({ blob: new Blob(chunks, { type }), mimeType: type, durationMs });\n chunks = [];\n };\n\n return {\n mimeType: negotiated,\n status: () => status,\n durationMs: elapsed,\n\n start(): void {\n if (status === \"recording\" || status === \"paused\") return;\n chunks = [];\n accumulatedMs = 0;\n discard = false;\n segmentStart = Date.now();\n status = \"recording\";\n if (timesliceMs !== undefined) recorder.start(timesliceMs);\n else recorder.start();\n },\n\n pause(): void {\n if (status !== \"recording\") return;\n accumulatedMs += Date.now() - segmentStart;\n status = \"paused\";\n recorder.pause();\n },\n\n resume(): void {\n if (status !== \"paused\") return;\n segmentStart = Date.now();\n status = \"recording\";\n recorder.resume();\n },\n\n stop(): Promise<MediaRecording> {\n if (status === \"idle\" || status === \"stopped\") {\n return Promise.resolve({\n blob: new Blob([], { type: negotiated }),\n mimeType: negotiated,\n durationMs: 0,\n });\n }\n // Stopping while paused needs no clock fix-up: `pause()` already folded\n // the last segment into `accumulatedMs`, and `elapsed()` adds nothing\n // while the status is not `\"recording\"`.\n return new Promise<MediaRecording>((resolve) => {\n settle = resolve;\n recorder.stop();\n });\n },\n\n cancel(): void {\n if (status === \"idle\" || status === \"stopped\") return;\n discard = true;\n settle = null;\n recorder.stop();\n },\n };\n}\n"],"mappings":";AA0EA,SAAgB,EAAsB,GAA6C;CAK/E,OAJI,OAAO,gBAAkB,MAAoB,OAG7C,OAAO,cAAc,mBAAoB,aACtC,EAAU,MAAM,MAAS,cAAc,gBAAgB,CAAI,CAAC,KAAK,OADR,EAAU,MAAM;AAEpF;AA4BA,SAAgB,EACZ,GACA,GACmB;CACnB,IAAM,EACF,eACA,SACA,UAAU,GACV,uBACA,uBACA,gBACA,YACA,eACA;CAEJ,IAAI,OAAO,gBAAkB,KACzB,MAAU,MAAM,qDAAqD;CAEzE,IAAM,IAAa,KAAU,EAAsB,CAAU;CAC7D,IAAI,MAAe,MACf,MAAU,MAAM,4CAA4C,EAAK,YAAY;CAEjF,IACI,MAAW,KAAA,KACX,OAAO,cAAc,mBAAoB,cACzC,CAAC,cAAc,gBAAgB,CAAM,GAErC,MAAU,MAAM,+BAA+B,EAAO,GAAG;CAG7D,IAAM,IAAW,IAAI,cAAc,GAAQ;EACvC,UAAU;EACV,GAAI,MAAuB,KAAA,IAAqC,CAAC,IAA1B,EAAE,sBAAmB;EAC5D,GAAI,MAAuB,KAAA,IAAqC,CAAC,IAA1B,EAAE,sBAAmB;CAChE,CAAC,GAEG,IAAiB,CAAC,GAClB,IAA8B,QAC9B,IAAgB,GAChB,IAAe,GACf,IAAuD,MACvD,IAAU,IAER,UACF,KAAiB,MAAW,cAAc,KAAK,IAAI,IAAI,IAAe;CA6B1E,OA3BA,EAAS,mBAAmB,MAA2B;EAC/C,EAAM,KAAK,SAAS,MACpB,MACJ,EAAO,KAAK,EAAM,IAAI,GACtB,IAAU,EAAM,IAAI;CACxB,GAEA,EAAS,WAAW,MAAuB;EACvC,IAAW,EAAyC,SAAS,CAAK;CACtE,GAEA,EAAS,eAAqB;EAC1B,IAAM,IAAa,EAAQ;EAC3B,IAAS;EACT,IAAM,IAAU;EAEhB,IADA,IAAS,MACL,GAAS;GACT,IAAS,CAAC;GACV;EACJ;EAGA,IAAM,IAAO,EAAS,YAAY;EAElC,AADA,IAAU;GAAE,MAAM,IAAI,KAAK,GAAQ,EAAE,QAAK,CAAC;GAAG,UAAU;GAAM;EAAW,CAAC,GAC1E,IAAS,CAAC;CACd,GAEO;EACH,UAAU;EACV,cAAc;EACd,YAAY;EAEZ,QAAc;GACN,MAAW,eAAe,MAAW,aACzC,IAAS,CAAC,GACV,IAAgB,GAChB,IAAU,IACV,IAAe,KAAK,IAAI,GACxB,IAAS,aACL,MAAgB,KAAA,IACf,EAAS,MAAM,IADW,EAAS,MAAM,CAAW;EAE7D;EAEA,QAAc;GACN,MAAW,gBACf,KAAiB,KAAK,IAAI,IAAI,GAC9B,IAAS,UACT,EAAS,MAAM;EACnB;EAEA,SAAe;GACP,MAAW,aACf,IAAe,KAAK,IAAI,GACxB,IAAS,aACT,EAAS,OAAO;EACpB;EAEA,OAAgC;GAW5B,OAVI,MAAW,UAAU,MAAW,YACzB,QAAQ,QAAQ;IACnB,MAAM,IAAI,KAAK,CAAC,GAAG,EAAE,MAAM,EAAW,CAAC;IACvC,UAAU;IACV,YAAY;GAChB,CAAC,IAKE,IAAI,SAAyB,MAAY;IAE5C,AADA,IAAS,GACT,EAAS,KAAK;GAClB,CAAC;EACL;EAEA,SAAe;GACP,MAAW,UAAU,MAAW,cACpC,IAAU,IACV,IAAS,MACT,EAAS,KAAK;EAClB;CACJ;AACJ"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-barcode-scanner.cjs","names":[],"sources":["../../src/capture/use-barcode-scanner.ts"],"sourcesContent":["import { useEffect, useRef, useState, type RefObject } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\nimport {\n useCameraStream,\n type CameraStreamError,\n type CameraStreamStatus,\n} from \"@/vision/use-camera-stream\";\n\nimport {\n createBarcodeDetector,\n getSupportedBarcodeFormats,\n isBarcodeDetectionSupported,\n normalizeBarcode,\n DEFAULT_BARCODE_FORMATS,\n type BarcodeDetectorLike,\n type BarcodeFormat,\n type BarcodeScanResult,\n} from \"./barcode\";\nimport { useTorch, type UseTorchResult } from \"./use-torch\";\n\n/** Options for {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerOptions {\n /** Symbologies to look for. Defaults to {@link DEFAULT_BARCODE_FORMATS}. */\n formats?: readonly BarcodeFormat[];\n /** Called for every accepted read — that is, after repeat suppression. */\n onScan?: (result: BarcodeScanResult) => void;\n /**\n * How often a frame is examined, in ms. Default 200.\n *\n * Not `requestAnimationFrame`: decoding is 10–40 ms of main-thread work on a\n * phone, so running it per frame competes with the preview it is reading from and\n * makes the video stutter. Five looks per second is faster than a human can aim.\n */\n intervalMs?: number;\n /**\n * Ignore the **same** value again for this long, in ms. Default 2500.\n *\n * A symbol stays in frame for as long as the user holds the camera there, so a\n * scanner without this fires the same code five times a second — which, wired to\n * \"add item to cart\", is a bug the user pays for. A *different* value is never\n * suppressed.\n */\n repeatDelayMs?: number;\n /** Stop looking without releasing the camera — a confirmation sheet is open. */\n paused?: boolean;\n /**\n * A decoder to use instead of the native one.\n *\n * The way to support Safari and Firefox: hand in a polyfill and everything else\n * here works unchanged. See {@link isBarcodeDetectionSupported} for why the SDK\n * does not bundle one.\n */\n detector?: BarcodeDetectorLike;\n /** Camera constraints, forwarded to `useCameraStream`. Defaults to the rear camera. */\n constraints?: MediaStreamConstraints;\n /**\n * A frame the engine refused to decode.\n *\n * Not \"nothing found\" — that resolves to an empty list and is the normal case.\n * This is the engine itself failing, which the loop survives because it is\n * usually transient (a frame arriving between two resolutions).\n */\n onError?: (error: unknown) => void;\n}\n\n/** Value returned by {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerResult {\n /** Attach to a `<video ref={…} muted playsInline />`. */\n videoRef: RefObject<HTMLVideoElement | null>;\n /** Camera lifecycle. `\"ready\"` means the preview is running. */\n status: CameraStreamStatus;\n /** Classified camera error, or `null`. */\n error: CameraStreamError | null;\n /** `false` when there is no decoder — no native API and none injected. */\n supported: boolean;\n /**\n * Formats actually in use: the requested ones intersected with what the engine\n * reports. Empty while the probe is in flight, or when nothing matched.\n */\n formats: readonly BarcodeFormat[];\n /** Whether the detect loop is running right now. */\n scanning: boolean;\n /** The most recent accepted read, or `null`. */\n result: BarcodeScanResult | null;\n /** The LED torch of this camera, when it has one. */\n torch: UseTorchResult;\n /** Re-open the camera after an error (the user changed the permission). */\n retry: () => void;\n}\n\n/** A video is decodable once it has data for the current frame and a real size. */\nfunction frameIsReady(video: HTMLVideoElement): boolean {\n return video.readyState >= 2 && video.videoWidth > 0 && video.videoHeight > 0;\n}\n\n/**\n * Read barcodes and QR codes from the camera.\n *\n * The camera and its classified errors come from `useCameraStream`, so this hook is\n * only the decoding half: it drives a `BarcodeDetector` over the preview on an\n * interval, suppresses the same value repeating, and exposes the torch.\n *\n * **Mounting this opens the camera.** It inherits that from `useCameraStream`, which\n * acquires on mount — so mount it *after* the user asks to scan (a button that reveals\n * the scanner), never on a page that merely contains one. A permission prompt nobody\n * provoked is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever prompting again.\n *\n * `supported` deserves a branch in the UI, not an assertion: `BarcodeDetector` is\n * Chromium-only and missing on Windows/Linux desktop, Firefox and everything on iOS.\n * Inject a `detector` to cover those, or tell the user to type the code.\n *\n * @param options - See {@link UseBarcodeScannerOptions}.\n * @returns The camera plumbing plus the scan state.\n *\n * @example\n * const scanner = useBarcodeScanner({\n * formats: [\"ean_13\"],\n * onScan: ({ rawValue }) => addToCart(rawValue),\n * });\n * return <video ref={scanner.videoRef} muted playsInline />;\n */\nexport function useBarcodeScanner(options: UseBarcodeScannerOptions = {}): UseBarcodeScannerResult {\n const {\n formats: requested = DEFAULT_BARCODE_FORMATS,\n onScan,\n intervalMs = 200,\n repeatDelayMs = 2500,\n paused = false,\n detector: injected,\n constraints,\n onError,\n } = options;\n\n const [supported, setSupported] = useState(\n () => injected !== undefined || isBarcodeDetectionSupported(),\n );\n\n /**\n * No decoder, no camera.\n *\n * Opening the camera only to report \"this browser cannot decode barcodes\" spends a\n * permission prompt on nothing — and a refusal is permanent, so it also spends the\n * *next* feature that needs the camera.\n */\n const camera = useCameraStream({ constraints, enabled: supported });\n const torch = useTorch(camera.stream);\n\n const [formats, setFormats] = useState<readonly BarcodeFormat[]>(\n injected !== undefined ? requested : [],\n );\n const [scanning, setScanning] = useState(false);\n const [result, setResult] = useState<BarcodeScanResult | null>(null);\n\n const detectorRef = useRef<BarcodeDetectorLike | null>(null);\n const lastValue = useRef<string | null>(null);\n const lastAt = useRef(0);\n\n const emitScan = useStableCallback((scan: BarcodeScanResult) => onScan?.(scan));\n const emitError = useStableCallback((error: unknown) => onError?.(error));\n\n const requestedKey = requested.join(\",\");\n\n /**\n * Resolve which formats the engine will take, then build the detector.\n *\n * The intersection is not defensive coding: `new BarcodeDetector({ formats })`\n * throws `NotSupportedError` when any entry is unknown to the platform decoder, and\n * that list differs between two Chromium builds on two operating systems. Asking\n * for the intersection is the only way one call site works everywhere.\n */\n useEffect(() => {\n if (injected) {\n detectorRef.current = injected;\n setSupported(true);\n setFormats(requested);\n return;\n }\n if (!isBarcodeDetectionSupported()) {\n detectorRef.current = null;\n setSupported(false);\n setFormats([]);\n return;\n }\n let cancelled = false;\n void getSupportedBarcodeFormats().then((available) => {\n if (cancelled) return;\n const usable =\n available.length === 0\n ? requested\n : requested.filter((format) => available.includes(format));\n const detector = usable.length > 0 ? createBarcodeDetector(usable) : null;\n detectorRef.current = detector;\n setFormats(detector ? usable : []);\n setSupported(detector !== null);\n });\n return () => {\n cancelled = true;\n };\n // `requestedKey` stands in for the array identity, so a caller passing an\n // inline `formats={[\"ean_13\"]}` does not rebuild the detector every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [injected, requestedKey]);\n\n /**\n * Look at a frame every `intervalMs`, and never overlap two looks.\n *\n * The loop re-arms itself *after* each `detect()` settles rather than running on a\n * fixed `setInterval`: decoding sometimes takes longer than the interval, and an\n * interval would then queue calls faster than the engine drains them until the tab\n * is unusable.\n */\n useEffect(() => {\n if (!supported || paused || camera.status !== \"ready\") {\n setScanning(false);\n return;\n }\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n setScanning(true);\n\n const accept = (scan: BarcodeScanResult): void => {\n const now = Date.now();\n const isRepeat =\n scan.rawValue === lastValue.current && now - lastAt.current < repeatDelayMs;\n if (isRepeat) return;\n lastValue.current = scan.rawValue;\n lastAt.current = now;\n setResult(scan);\n emitScan(scan);\n };\n\n const look = async (): Promise<void> => {\n const video = camera.videoRef.current;\n const detector = detectorRef.current;\n if (!video || !detector || !frameIsReady(video)) return;\n try {\n const found = await detector.detect(video);\n if (stopped) return;\n for (const raw of found) {\n const scan = normalizeBarcode(raw);\n if (scan.rawValue !== \"\") accept(scan);\n }\n } catch (error) {\n if (!stopped) emitError(error);\n }\n };\n\n const tick = (): void => {\n void look().finally(() => {\n if (!stopped) timer = setTimeout(tick, intervalMs);\n });\n };\n tick();\n\n return () => {\n stopped = true;\n if (timer !== undefined) clearTimeout(timer);\n setScanning(false);\n };\n }, [\n supported,\n paused,\n camera.status,\n camera.videoRef,\n intervalMs,\n repeatDelayMs,\n emitScan,\n emitError,\n ]);\n\n return {\n videoRef: camera.videoRef,\n status: camera.status,\n error: camera.error,\n supported,\n formats,\n scanning,\n result,\n torch,\n retry: camera.retry,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-barcode-scanner.cjs","names":[],"sources":["../../src/capture/use-barcode-scanner.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — the scan loop, the repeat suppression window and the\n * torch control share the same track and the same interval handle; splitting them\n * would leave a timer running against a track another hook stopped.\n */\nimport { useEffect, useRef, useState, type RefObject } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\nimport {\n useCameraStream,\n type CameraStreamError,\n type CameraStreamStatus,\n} from \"@/vision/use-camera-stream\";\n\nimport {\n createBarcodeDetector,\n getSupportedBarcodeFormats,\n isBarcodeDetectionSupported,\n normalizeBarcode,\n DEFAULT_BARCODE_FORMATS,\n type BarcodeDetectorLike,\n type BarcodeFormat,\n type BarcodeScanResult,\n} from \"./barcode\";\nimport { useTorch, type UseTorchResult } from \"./use-torch\";\n\n/** Options for {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerOptions {\n /** Symbologies to look for. Defaults to {@link DEFAULT_BARCODE_FORMATS}. */\n formats?: readonly BarcodeFormat[];\n /** Called for every accepted read — that is, after repeat suppression. */\n onScan?: (result: BarcodeScanResult) => void;\n /**\n * How often a frame is examined, in ms. Default 200.\n *\n * Not `requestAnimationFrame`: decoding is 10–40 ms of main-thread work on a\n * phone, so running it per frame competes with the preview it is reading from and\n * makes the video stutter. Five looks per second is faster than a human can aim.\n */\n intervalMs?: number;\n /**\n * Ignore the **same** value again for this long, in ms. Default 2500.\n *\n * A symbol stays in frame for as long as the user holds the camera there, so a\n * scanner without this fires the same code five times a second — which, wired to\n * \"add item to cart\", is a bug the user pays for. A *different* value is never\n * suppressed.\n */\n repeatDelayMs?: number;\n /** Stop looking without releasing the camera — a confirmation sheet is open. */\n paused?: boolean;\n /**\n * A decoder to use instead of the native one.\n *\n * The way to support Safari and Firefox: hand in a polyfill and everything else\n * here works unchanged. See {@link isBarcodeDetectionSupported} for why the SDK\n * does not bundle one.\n */\n detector?: BarcodeDetectorLike;\n /** Camera constraints, forwarded to `useCameraStream`. Defaults to the rear camera. */\n constraints?: MediaStreamConstraints;\n /**\n * A frame the engine refused to decode.\n *\n * Not \"nothing found\" — that resolves to an empty list and is the normal case.\n * This is the engine itself failing, which the loop survives because it is\n * usually transient (a frame arriving between two resolutions).\n */\n onError?: (error: unknown) => void;\n}\n\n/** Value returned by {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerResult {\n /** Attach to a `<video ref={…} muted playsInline />`. */\n videoRef: RefObject<HTMLVideoElement | null>;\n /** Camera lifecycle. `\"ready\"` means the preview is running. */\n status: CameraStreamStatus;\n /** Classified camera error, or `null`. */\n error: CameraStreamError | null;\n /** `false` when there is no decoder — no native API and none injected. */\n supported: boolean;\n /**\n * Formats actually in use: the requested ones intersected with what the engine\n * reports. Empty while the probe is in flight, or when nothing matched.\n */\n formats: readonly BarcodeFormat[];\n /** Whether the detect loop is running right now. */\n scanning: boolean;\n /** The most recent accepted read, or `null`. */\n result: BarcodeScanResult | null;\n /** The LED torch of this camera, when it has one. */\n torch: UseTorchResult;\n /** Re-open the camera after an error (the user changed the permission). */\n retry: () => void;\n}\n\n/** A video is decodable once it has data for the current frame and a real size. */\nfunction frameIsReady(video: HTMLVideoElement): boolean {\n return video.readyState >= 2 && video.videoWidth > 0 && video.videoHeight > 0;\n}\n\n/**\n * Read barcodes and QR codes from the camera.\n *\n * The camera and its classified errors come from `useCameraStream`, so this hook is\n * only the decoding half: it drives a `BarcodeDetector` over the preview on an\n * interval, suppresses the same value repeating, and exposes the torch.\n *\n * **Mounting this opens the camera.** It inherits that from `useCameraStream`, which\n * acquires on mount — so mount it *after* the user asks to scan (a button that reveals\n * the scanner), never on a page that merely contains one. A permission prompt nobody\n * provoked is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever prompting again.\n *\n * `supported` deserves a branch in the UI, not an assertion: `BarcodeDetector` is\n * Chromium-only and missing on Windows/Linux desktop, Firefox and everything on iOS.\n * Inject a `detector` to cover those, or tell the user to type the code.\n *\n * @param options - See {@link UseBarcodeScannerOptions}.\n * @returns The camera plumbing plus the scan state.\n *\n * @example\n * const scanner = useBarcodeScanner({\n * formats: [\"ean_13\"],\n * onScan: ({ rawValue }) => addToCart(rawValue),\n * });\n * return <video ref={scanner.videoRef} muted playsInline />;\n */\nexport function useBarcodeScanner(options: UseBarcodeScannerOptions = {}): UseBarcodeScannerResult {\n const {\n formats: requested = DEFAULT_BARCODE_FORMATS,\n onScan,\n intervalMs = 200,\n repeatDelayMs = 2500,\n paused = false,\n detector: injected,\n constraints,\n onError,\n } = options;\n\n const [supported, setSupported] = useState(\n () => injected !== undefined || isBarcodeDetectionSupported(),\n );\n\n /**\n * No decoder, no camera.\n *\n * Opening the camera only to report \"this browser cannot decode barcodes\" spends a\n * permission prompt on nothing — and a refusal is permanent, so it also spends the\n * *next* feature that needs the camera.\n */\n const camera = useCameraStream({ constraints, enabled: supported });\n const torch = useTorch(camera.stream);\n\n const [formats, setFormats] = useState<readonly BarcodeFormat[]>(\n injected !== undefined ? requested : [],\n );\n const [scanning, setScanning] = useState(false);\n const [result, setResult] = useState<BarcodeScanResult | null>(null);\n\n const detectorRef = useRef<BarcodeDetectorLike | null>(null);\n const lastValue = useRef<string | null>(null);\n const lastAt = useRef(0);\n\n const emitScan = useStableCallback((scan: BarcodeScanResult) => onScan?.(scan));\n const emitError = useStableCallback((error: unknown) => onError?.(error));\n\n const requestedKey = requested.join(\",\");\n\n /**\n * Resolve which formats the engine will take, then build the detector.\n *\n * The intersection is not defensive coding: `new BarcodeDetector({ formats })`\n * throws `NotSupportedError` when any entry is unknown to the platform decoder, and\n * that list differs between two Chromium builds on two operating systems. Asking\n * for the intersection is the only way one call site works everywhere.\n */\n useEffect(() => {\n if (injected) {\n detectorRef.current = injected;\n setSupported(true);\n setFormats(requested);\n return;\n }\n if (!isBarcodeDetectionSupported()) {\n detectorRef.current = null;\n setSupported(false);\n setFormats([]);\n return;\n }\n let cancelled = false;\n void getSupportedBarcodeFormats().then((available) => {\n if (cancelled) return;\n const usable =\n available.length === 0\n ? requested\n : requested.filter((format) => available.includes(format));\n const detector = usable.length > 0 ? createBarcodeDetector(usable) : null;\n detectorRef.current = detector;\n setFormats(detector ? usable : []);\n setSupported(detector !== null);\n });\n return () => {\n cancelled = true;\n };\n // `requestedKey` stands in for the array identity, so a caller passing an\n // inline `formats={[\"ean_13\"]}` does not rebuild the detector every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [injected, requestedKey]);\n\n /**\n * Look at a frame every `intervalMs`, and never overlap two looks.\n *\n * The loop re-arms itself *after* each `detect()` settles rather than running on a\n * fixed `setInterval`: decoding sometimes takes longer than the interval, and an\n * interval would then queue calls faster than the engine drains them until the tab\n * is unusable.\n */\n useEffect(() => {\n if (!supported || paused || camera.status !== \"ready\") {\n setScanning(false);\n return;\n }\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n setScanning(true);\n\n const accept = (scan: BarcodeScanResult): void => {\n const now = Date.now();\n const isRepeat =\n scan.rawValue === lastValue.current && now - lastAt.current < repeatDelayMs;\n if (isRepeat) return;\n lastValue.current = scan.rawValue;\n lastAt.current = now;\n setResult(scan);\n emitScan(scan);\n };\n\n const look = async (): Promise<void> => {\n const video = camera.videoRef.current;\n const detector = detectorRef.current;\n if (!video || !detector || !frameIsReady(video)) return;\n try {\n const found = await detector.detect(video);\n if (stopped) return;\n for (const raw of found) {\n const scan = normalizeBarcode(raw);\n if (scan.rawValue !== \"\") accept(scan);\n }\n } catch (error) {\n if (!stopped) emitError(error);\n }\n };\n\n const tick = (): void => {\n void look().finally(() => {\n if (!stopped) timer = setTimeout(tick, intervalMs);\n });\n };\n tick();\n\n return () => {\n stopped = true;\n if (timer !== undefined) clearTimeout(timer);\n setScanning(false);\n };\n }, [\n supported,\n paused,\n camera.status,\n camera.videoRef,\n intervalMs,\n repeatDelayMs,\n emitScan,\n emitError,\n ]);\n\n return {\n videoRef: camera.videoRef,\n status: camera.status,\n error: camera.error,\n supported,\n formats,\n scanning,\n result,\n torch,\n retry: camera.retry,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-barcode-scanner.js","names":[],"sources":["../../src/capture/use-barcode-scanner.ts"],"sourcesContent":["import { useEffect, useRef, useState, type RefObject } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\nimport {\n useCameraStream,\n type CameraStreamError,\n type CameraStreamStatus,\n} from \"@/vision/use-camera-stream\";\n\nimport {\n createBarcodeDetector,\n getSupportedBarcodeFormats,\n isBarcodeDetectionSupported,\n normalizeBarcode,\n DEFAULT_BARCODE_FORMATS,\n type BarcodeDetectorLike,\n type BarcodeFormat,\n type BarcodeScanResult,\n} from \"./barcode\";\nimport { useTorch, type UseTorchResult } from \"./use-torch\";\n\n/** Options for {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerOptions {\n /** Symbologies to look for. Defaults to {@link DEFAULT_BARCODE_FORMATS}. */\n formats?: readonly BarcodeFormat[];\n /** Called for every accepted read — that is, after repeat suppression. */\n onScan?: (result: BarcodeScanResult) => void;\n /**\n * How often a frame is examined, in ms. Default 200.\n *\n * Not `requestAnimationFrame`: decoding is 10–40 ms of main-thread work on a\n * phone, so running it per frame competes with the preview it is reading from and\n * makes the video stutter. Five looks per second is faster than a human can aim.\n */\n intervalMs?: number;\n /**\n * Ignore the **same** value again for this long, in ms. Default 2500.\n *\n * A symbol stays in frame for as long as the user holds the camera there, so a\n * scanner without this fires the same code five times a second — which, wired to\n * \"add item to cart\", is a bug the user pays for. A *different* value is never\n * suppressed.\n */\n repeatDelayMs?: number;\n /** Stop looking without releasing the camera — a confirmation sheet is open. */\n paused?: boolean;\n /**\n * A decoder to use instead of the native one.\n *\n * The way to support Safari and Firefox: hand in a polyfill and everything else\n * here works unchanged. See {@link isBarcodeDetectionSupported} for why the SDK\n * does not bundle one.\n */\n detector?: BarcodeDetectorLike;\n /** Camera constraints, forwarded to `useCameraStream`. Defaults to the rear camera. */\n constraints?: MediaStreamConstraints;\n /**\n * A frame the engine refused to decode.\n *\n * Not \"nothing found\" — that resolves to an empty list and is the normal case.\n * This is the engine itself failing, which the loop survives because it is\n * usually transient (a frame arriving between two resolutions).\n */\n onError?: (error: unknown) => void;\n}\n\n/** Value returned by {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerResult {\n /** Attach to a `<video ref={…} muted playsInline />`. */\n videoRef: RefObject<HTMLVideoElement | null>;\n /** Camera lifecycle. `\"ready\"` means the preview is running. */\n status: CameraStreamStatus;\n /** Classified camera error, or `null`. */\n error: CameraStreamError | null;\n /** `false` when there is no decoder — no native API and none injected. */\n supported: boolean;\n /**\n * Formats actually in use: the requested ones intersected with what the engine\n * reports. Empty while the probe is in flight, or when nothing matched.\n */\n formats: readonly BarcodeFormat[];\n /** Whether the detect loop is running right now. */\n scanning: boolean;\n /** The most recent accepted read, or `null`. */\n result: BarcodeScanResult | null;\n /** The LED torch of this camera, when it has one. */\n torch: UseTorchResult;\n /** Re-open the camera after an error (the user changed the permission). */\n retry: () => void;\n}\n\n/** A video is decodable once it has data for the current frame and a real size. */\nfunction frameIsReady(video: HTMLVideoElement): boolean {\n return video.readyState >= 2 && video.videoWidth > 0 && video.videoHeight > 0;\n}\n\n/**\n * Read barcodes and QR codes from the camera.\n *\n * The camera and its classified errors come from `useCameraStream`, so this hook is\n * only the decoding half: it drives a `BarcodeDetector` over the preview on an\n * interval, suppresses the same value repeating, and exposes the torch.\n *\n * **Mounting this opens the camera.** It inherits that from `useCameraStream`, which\n * acquires on mount — so mount it *after* the user asks to scan (a button that reveals\n * the scanner), never on a page that merely contains one. A permission prompt nobody\n * provoked is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever prompting again.\n *\n * `supported` deserves a branch in the UI, not an assertion: `BarcodeDetector` is\n * Chromium-only and missing on Windows/Linux desktop, Firefox and everything on iOS.\n * Inject a `detector` to cover those, or tell the user to type the code.\n *\n * @param options - See {@link UseBarcodeScannerOptions}.\n * @returns The camera plumbing plus the scan state.\n *\n * @example\n * const scanner = useBarcodeScanner({\n * formats: [\"ean_13\"],\n * onScan: ({ rawValue }) => addToCart(rawValue),\n * });\n * return <video ref={scanner.videoRef} muted playsInline />;\n */\nexport function useBarcodeScanner(options: UseBarcodeScannerOptions = {}): UseBarcodeScannerResult {\n const {\n formats: requested = DEFAULT_BARCODE_FORMATS,\n onScan,\n intervalMs = 200,\n repeatDelayMs = 2500,\n paused = false,\n detector: injected,\n constraints,\n onError,\n } = options;\n\n const [supported, setSupported] = useState(\n () => injected !== undefined || isBarcodeDetectionSupported(),\n );\n\n /**\n * No decoder, no camera.\n *\n * Opening the camera only to report \"this browser cannot decode barcodes\" spends a\n * permission prompt on nothing — and a refusal is permanent, so it also spends the\n * *next* feature that needs the camera.\n */\n const camera = useCameraStream({ constraints, enabled: supported });\n const torch = useTorch(camera.stream);\n\n const [formats, setFormats] = useState<readonly BarcodeFormat[]>(\n injected !== undefined ? requested : [],\n );\n const [scanning, setScanning] = useState(false);\n const [result, setResult] = useState<BarcodeScanResult | null>(null);\n\n const detectorRef = useRef<BarcodeDetectorLike | null>(null);\n const lastValue = useRef<string | null>(null);\n const lastAt = useRef(0);\n\n const emitScan = useStableCallback((scan: BarcodeScanResult) => onScan?.(scan));\n const emitError = useStableCallback((error: unknown) => onError?.(error));\n\n const requestedKey = requested.join(\",\");\n\n /**\n * Resolve which formats the engine will take, then build the detector.\n *\n * The intersection is not defensive coding: `new BarcodeDetector({ formats })`\n * throws `NotSupportedError` when any entry is unknown to the platform decoder, and\n * that list differs between two Chromium builds on two operating systems. Asking\n * for the intersection is the only way one call site works everywhere.\n */\n useEffect(() => {\n if (injected) {\n detectorRef.current = injected;\n setSupported(true);\n setFormats(requested);\n return;\n }\n if (!isBarcodeDetectionSupported()) {\n detectorRef.current = null;\n setSupported(false);\n setFormats([]);\n return;\n }\n let cancelled = false;\n void getSupportedBarcodeFormats().then((available) => {\n if (cancelled) return;\n const usable =\n available.length === 0\n ? requested\n : requested.filter((format) => available.includes(format));\n const detector = usable.length > 0 ? createBarcodeDetector(usable) : null;\n detectorRef.current = detector;\n setFormats(detector ? usable : []);\n setSupported(detector !== null);\n });\n return () => {\n cancelled = true;\n };\n // `requestedKey` stands in for the array identity, so a caller passing an\n // inline `formats={[\"ean_13\"]}` does not rebuild the detector every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [injected, requestedKey]);\n\n /**\n * Look at a frame every `intervalMs`, and never overlap two looks.\n *\n * The loop re-arms itself *after* each `detect()` settles rather than running on a\n * fixed `setInterval`: decoding sometimes takes longer than the interval, and an\n * interval would then queue calls faster than the engine drains them until the tab\n * is unusable.\n */\n useEffect(() => {\n if (!supported || paused || camera.status !== \"ready\") {\n setScanning(false);\n return;\n }\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n setScanning(true);\n\n const accept = (scan: BarcodeScanResult): void => {\n const now = Date.now();\n const isRepeat =\n scan.rawValue === lastValue.current && now - lastAt.current < repeatDelayMs;\n if (isRepeat) return;\n lastValue.current = scan.rawValue;\n lastAt.current = now;\n setResult(scan);\n emitScan(scan);\n };\n\n const look = async (): Promise<void> => {\n const video = camera.videoRef.current;\n const detector = detectorRef.current;\n if (!video || !detector || !frameIsReady(video)) return;\n try {\n const found = await detector.detect(video);\n if (stopped) return;\n for (const raw of found) {\n const scan = normalizeBarcode(raw);\n if (scan.rawValue !== \"\") accept(scan);\n }\n } catch (error) {\n if (!stopped) emitError(error);\n }\n };\n\n const tick = (): void => {\n void look().finally(() => {\n if (!stopped) timer = setTimeout(tick, intervalMs);\n });\n };\n tick();\n\n return () => {\n stopped = true;\n if (timer !== undefined) clearTimeout(timer);\n setScanning(false);\n };\n }, [\n supported,\n paused,\n camera.status,\n camera.videoRef,\n intervalMs,\n repeatDelayMs,\n emitScan,\n emitError,\n ]);\n\n return {\n videoRef: camera.videoRef,\n status: camera.status,\n error: camera.error,\n supported,\n formats,\n scanning,\n result,\n torch,\n retry: camera.retry,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-barcode-scanner.js","names":[],"sources":["../../src/capture/use-barcode-scanner.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — the scan loop, the repeat suppression window and the\n * torch control share the same track and the same interval handle; splitting them\n * would leave a timer running against a track another hook stopped.\n */\nimport { useEffect, useRef, useState, type RefObject } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\nimport {\n useCameraStream,\n type CameraStreamError,\n type CameraStreamStatus,\n} from \"@/vision/use-camera-stream\";\n\nimport {\n createBarcodeDetector,\n getSupportedBarcodeFormats,\n isBarcodeDetectionSupported,\n normalizeBarcode,\n DEFAULT_BARCODE_FORMATS,\n type BarcodeDetectorLike,\n type BarcodeFormat,\n type BarcodeScanResult,\n} from \"./barcode\";\nimport { useTorch, type UseTorchResult } from \"./use-torch\";\n\n/** Options for {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerOptions {\n /** Symbologies to look for. Defaults to {@link DEFAULT_BARCODE_FORMATS}. */\n formats?: readonly BarcodeFormat[];\n /** Called for every accepted read — that is, after repeat suppression. */\n onScan?: (result: BarcodeScanResult) => void;\n /**\n * How often a frame is examined, in ms. Default 200.\n *\n * Not `requestAnimationFrame`: decoding is 10–40 ms of main-thread work on a\n * phone, so running it per frame competes with the preview it is reading from and\n * makes the video stutter. Five looks per second is faster than a human can aim.\n */\n intervalMs?: number;\n /**\n * Ignore the **same** value again for this long, in ms. Default 2500.\n *\n * A symbol stays in frame for as long as the user holds the camera there, so a\n * scanner without this fires the same code five times a second — which, wired to\n * \"add item to cart\", is a bug the user pays for. A *different* value is never\n * suppressed.\n */\n repeatDelayMs?: number;\n /** Stop looking without releasing the camera — a confirmation sheet is open. */\n paused?: boolean;\n /**\n * A decoder to use instead of the native one.\n *\n * The way to support Safari and Firefox: hand in a polyfill and everything else\n * here works unchanged. See {@link isBarcodeDetectionSupported} for why the SDK\n * does not bundle one.\n */\n detector?: BarcodeDetectorLike;\n /** Camera constraints, forwarded to `useCameraStream`. Defaults to the rear camera. */\n constraints?: MediaStreamConstraints;\n /**\n * A frame the engine refused to decode.\n *\n * Not \"nothing found\" — that resolves to an empty list and is the normal case.\n * This is the engine itself failing, which the loop survives because it is\n * usually transient (a frame arriving between two resolutions).\n */\n onError?: (error: unknown) => void;\n}\n\n/** Value returned by {@link useBarcodeScanner}. */\nexport interface UseBarcodeScannerResult {\n /** Attach to a `<video ref={…} muted playsInline />`. */\n videoRef: RefObject<HTMLVideoElement | null>;\n /** Camera lifecycle. `\"ready\"` means the preview is running. */\n status: CameraStreamStatus;\n /** Classified camera error, or `null`. */\n error: CameraStreamError | null;\n /** `false` when there is no decoder — no native API and none injected. */\n supported: boolean;\n /**\n * Formats actually in use: the requested ones intersected with what the engine\n * reports. Empty while the probe is in flight, or when nothing matched.\n */\n formats: readonly BarcodeFormat[];\n /** Whether the detect loop is running right now. */\n scanning: boolean;\n /** The most recent accepted read, or `null`. */\n result: BarcodeScanResult | null;\n /** The LED torch of this camera, when it has one. */\n torch: UseTorchResult;\n /** Re-open the camera after an error (the user changed the permission). */\n retry: () => void;\n}\n\n/** A video is decodable once it has data for the current frame and a real size. */\nfunction frameIsReady(video: HTMLVideoElement): boolean {\n return video.readyState >= 2 && video.videoWidth > 0 && video.videoHeight > 0;\n}\n\n/**\n * Read barcodes and QR codes from the camera.\n *\n * The camera and its classified errors come from `useCameraStream`, so this hook is\n * only the decoding half: it drives a `BarcodeDetector` over the preview on an\n * interval, suppresses the same value repeating, and exposes the torch.\n *\n * **Mounting this opens the camera.** It inherits that from `useCameraStream`, which\n * acquires on mount — so mount it *after* the user asks to scan (a button that reveals\n * the scanner), never on a page that merely contains one. A permission prompt nobody\n * provoked is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever prompting again.\n *\n * `supported` deserves a branch in the UI, not an assertion: `BarcodeDetector` is\n * Chromium-only and missing on Windows/Linux desktop, Firefox and everything on iOS.\n * Inject a `detector` to cover those, or tell the user to type the code.\n *\n * @param options - See {@link UseBarcodeScannerOptions}.\n * @returns The camera plumbing plus the scan state.\n *\n * @example\n * const scanner = useBarcodeScanner({\n * formats: [\"ean_13\"],\n * onScan: ({ rawValue }) => addToCart(rawValue),\n * });\n * return <video ref={scanner.videoRef} muted playsInline />;\n */\nexport function useBarcodeScanner(options: UseBarcodeScannerOptions = {}): UseBarcodeScannerResult {\n const {\n formats: requested = DEFAULT_BARCODE_FORMATS,\n onScan,\n intervalMs = 200,\n repeatDelayMs = 2500,\n paused = false,\n detector: injected,\n constraints,\n onError,\n } = options;\n\n const [supported, setSupported] = useState(\n () => injected !== undefined || isBarcodeDetectionSupported(),\n );\n\n /**\n * No decoder, no camera.\n *\n * Opening the camera only to report \"this browser cannot decode barcodes\" spends a\n * permission prompt on nothing — and a refusal is permanent, so it also spends the\n * *next* feature that needs the camera.\n */\n const camera = useCameraStream({ constraints, enabled: supported });\n const torch = useTorch(camera.stream);\n\n const [formats, setFormats] = useState<readonly BarcodeFormat[]>(\n injected !== undefined ? requested : [],\n );\n const [scanning, setScanning] = useState(false);\n const [result, setResult] = useState<BarcodeScanResult | null>(null);\n\n const detectorRef = useRef<BarcodeDetectorLike | null>(null);\n const lastValue = useRef<string | null>(null);\n const lastAt = useRef(0);\n\n const emitScan = useStableCallback((scan: BarcodeScanResult) => onScan?.(scan));\n const emitError = useStableCallback((error: unknown) => onError?.(error));\n\n const requestedKey = requested.join(\",\");\n\n /**\n * Resolve which formats the engine will take, then build the detector.\n *\n * The intersection is not defensive coding: `new BarcodeDetector({ formats })`\n * throws `NotSupportedError` when any entry is unknown to the platform decoder, and\n * that list differs between two Chromium builds on two operating systems. Asking\n * for the intersection is the only way one call site works everywhere.\n */\n useEffect(() => {\n if (injected) {\n detectorRef.current = injected;\n setSupported(true);\n setFormats(requested);\n return;\n }\n if (!isBarcodeDetectionSupported()) {\n detectorRef.current = null;\n setSupported(false);\n setFormats([]);\n return;\n }\n let cancelled = false;\n void getSupportedBarcodeFormats().then((available) => {\n if (cancelled) return;\n const usable =\n available.length === 0\n ? requested\n : requested.filter((format) => available.includes(format));\n const detector = usable.length > 0 ? createBarcodeDetector(usable) : null;\n detectorRef.current = detector;\n setFormats(detector ? usable : []);\n setSupported(detector !== null);\n });\n return () => {\n cancelled = true;\n };\n // `requestedKey` stands in for the array identity, so a caller passing an\n // inline `formats={[\"ean_13\"]}` does not rebuild the detector every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [injected, requestedKey]);\n\n /**\n * Look at a frame every `intervalMs`, and never overlap two looks.\n *\n * The loop re-arms itself *after* each `detect()` settles rather than running on a\n * fixed `setInterval`: decoding sometimes takes longer than the interval, and an\n * interval would then queue calls faster than the engine drains them until the tab\n * is unusable.\n */\n useEffect(() => {\n if (!supported || paused || camera.status !== \"ready\") {\n setScanning(false);\n return;\n }\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n setScanning(true);\n\n const accept = (scan: BarcodeScanResult): void => {\n const now = Date.now();\n const isRepeat =\n scan.rawValue === lastValue.current && now - lastAt.current < repeatDelayMs;\n if (isRepeat) return;\n lastValue.current = scan.rawValue;\n lastAt.current = now;\n setResult(scan);\n emitScan(scan);\n };\n\n const look = async (): Promise<void> => {\n const video = camera.videoRef.current;\n const detector = detectorRef.current;\n if (!video || !detector || !frameIsReady(video)) return;\n try {\n const found = await detector.detect(video);\n if (stopped) return;\n for (const raw of found) {\n const scan = normalizeBarcode(raw);\n if (scan.rawValue !== \"\") accept(scan);\n }\n } catch (error) {\n if (!stopped) emitError(error);\n }\n };\n\n const tick = (): void => {\n void look().finally(() => {\n if (!stopped) timer = setTimeout(tick, intervalMs);\n });\n };\n tick();\n\n return () => {\n stopped = true;\n if (timer !== undefined) clearTimeout(timer);\n setScanning(false);\n };\n }, [\n supported,\n paused,\n camera.status,\n camera.videoRef,\n intervalMs,\n repeatDelayMs,\n emitScan,\n emitError,\n ]);\n\n return {\n videoRef: camera.videoRef,\n status: camera.status,\n error: camera.error,\n supported,\n formats,\n scanning,\n result,\n torch,\n retry: camera.retry,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-screen-capture.cjs","names":[],"sources":["../../src/capture/use-screen-capture.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n classifyMediaError,\n missingCaptureApiError,\n type MediaAccessError,\n} from \"@/audio/media-access\";\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\n/** Lifecycle of a screen share. */\nexport type ScreenCaptureStatus = \"idle\" | \"requesting\" | \"sharing\" | \"error\";\n\n/** Which surface to put first in the picker. */\nexport type DisplaySurfaceHint = \"monitor\" | \"window\" | \"browser\";\n\n/**\n * Hints `getDisplayMedia` takes that TypeScript's `DisplayMediaStreamOptions` does not\n * list yet.\n */\ntype DisplayMediaOptionsWithHints = DisplayMediaStreamOptions & {\n preferCurrentTab?: boolean;\n selfBrowserSurface?: \"include\" | \"exclude\";\n surfaceSwitching?: \"include\" | \"exclude\";\n systemAudio?: \"include\" | \"exclude\";\n};\n\n/** `displaySurface` is a constrainable property of a display track, but not in the DOM lib. */\ntype DisplayVideoConstraints = MediaTrackConstraints & { displaySurface?: DisplaySurfaceHint };\n\n/** Options for {@link useScreenCapture}. */\nexport interface UseScreenCaptureOptions {\n /**\n * Capture the tab's audio too. Default `false`.\n *\n * Chromium only offers this for a **tab** — sharing a window or a whole screen\n * yields no audio track no matter what you ask for, and Safari has no display\n * audio at all. Ask for it and check what you got.\n */\n audio?: boolean;\n /**\n * Which surface the picker should offer first — `\"browser\"` is a tab.\n *\n * A hint, never a guarantee: the user can always pick something else, and Firefox\n * ignores it. Read `surface` afterwards to learn what actually happened.\n */\n displaySurface?: DisplaySurfaceHint;\n /**\n * Put *this* tab at the top of the picker. Default `false`.\n *\n * The right setting for \"record what you are seeing right now\" in a support flow.\n * Chromium only.\n */\n preferCurrentTab?: boolean;\n /** Offer this tab in the list at all. `\"exclude\"` prevents the hall-of-mirrors capture. */\n selfBrowserSurface?: \"include\" | \"exclude\";\n /** Let the user switch to a different surface mid-share, without a new prompt. */\n surfaceSwitching?: \"include\" | \"exclude\";\n /** Include the system audio when a whole screen is shared. Chromium, Windows only. */\n systemAudio?: \"include\" | \"exclude\";\n /** Escape hatch: full options, replacing everything above. */\n options?: DisplayMediaStreamOptions;\n /**\n * The user stopped the share from the browser's own bar.\n *\n * The single most important callback here — see the note on\n * {@link useScreenCapture}.\n */\n onEnded?: () => void;\n /**\n * The user dismissed the picker. **Not an error.**\n *\n * Receives the rejection so an app that needs to tell a dismissal from an OS-level\n * block (macOS screen-recording permission) can look at the message. Most should\n * simply return the UI to its previous state.\n */\n onCancelled?: (reason: unknown) => void;\n}\n\n/** Value returned by {@link useScreenCapture}. */\nexport interface UseScreenCaptureResult {\n status: ScreenCaptureStatus;\n /** The live stream, or `null`. Feed it to {@link useVideoRecorder} or a `<video>`. */\n stream: MediaStream | null;\n /** Classified error, or `null`. A cancelled picker leaves this `null`. */\n error: MediaAccessError | null;\n /** What the user actually picked, when the browser reports it. */\n surface: string | null;\n /** Whether the stream carries an audio track — ask, do not assume. */\n hasAudio: boolean;\n /** `false` when `getDisplayMedia` is missing (every browser on iOS, insecure pages). */\n supported: boolean;\n /** Open the picker. Must be called from a user gesture. */\n start: () => void;\n /** Stop sharing from the app side. Fires nothing — you asked for it. */\n stop: () => void;\n}\n\n/** Whether `getDisplayMedia` is reachable at all. */\nexport function isScreenCaptureSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n navigator.mediaDevices !== undefined &&\n typeof navigator.mediaDevices.getDisplayMedia === \"function\"\n );\n}\n\n/**\n * A rejection from `getDisplayMedia` that means \"the user said no thanks\".\n *\n * There is no distinct exception for a dismissed picker: closing it produces the same\n * `NotAllowedError` as a policy block, and some builds report `AbortError` instead. The\n * useful default is therefore to treat both as a cancellation, because a\n * display-capture prompt is **always** user-initiated — nothing can open it behind\n * their back — so the overwhelmingly likely cause is that they changed their mind, and\n * a red error toast for that punishes them for it. The rejection is handed to\n * `onCancelled` so the rarer causes stay diagnosable.\n */\nfunction isCancellation(err: unknown): boolean {\n return (\n err instanceof DOMException && (err.name === \"NotAllowedError\" || err.name === \"AbortError\")\n );\n}\n\n/**\n * Capture a screen, a window or a tab with `getDisplayMedia`.\n *\n * Three states decide whether this feels right, and two of them are easy to miss:\n *\n * - **The user dismissed the picker.** A rejection, but not a failure. It leaves\n * `error` at `null` and the status back at `\"idle\"`, and calls `onCancelled`.\n * - **The user stopped the share from the browser's own bar.** Nothing in your UI was\n * clicked and no promise rejects — the *only* signal is the video track's `ended`\n * event, so the hook listens for it and clears the stream. Without that listener, an\n * app shows \"gravando\" over a stream that is already dead.\n * - **The share is live.** `surface` says what was picked and `hasAudio` says whether\n * audio actually came along, which is not what you asked for but what you got.\n *\n * The stream is owned here: `stop()` and unmount both release every track. A recorder\n * built on it (`useVideoRecorder`) deliberately does not, so stopping a recording\n * leaves the share running for the next take.\n *\n * @param options - See {@link UseScreenCaptureOptions}.\n * @returns The stream, its status, a classified error and `start`/`stop`.\n *\n * @example\n * const screen = useScreenCapture({ preferCurrentTab: true, onEnded: () => save() });\n * const rec = useVideoRecorder(screen.stream);\n * <button onClick={screen.start}>Compartilhar tela</button>\n */\nexport function useScreenCapture(options: UseScreenCaptureOptions = {}): UseScreenCaptureResult {\n const {\n audio = false,\n displaySurface,\n preferCurrentTab,\n selfBrowserSurface,\n surfaceSwitching,\n systemAudio,\n options: raw,\n onEnded,\n onCancelled,\n } = options;\n\n const [status, setStatus] = useState<ScreenCaptureStatus>(\"idle\");\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [error, setError] = useState<MediaAccessError | null>(null);\n const [surface, setSurface] = useState<string | null>(null);\n const [hasAudio, setHasAudio] = useState(false);\n const [supported] = useState(isScreenCaptureSupported);\n\n const streamRef = useRef<MediaStream | null>(null);\n const detach = useRef<(() => void) | null>(null);\n const generation = useRef(0);\n\n const emitEnded = useStableCallback(() => onEnded?.());\n const emitCancelled = useStableCallback((reason: unknown) => onCancelled?.(reason));\n\n const release = useCallback((): void => {\n detach.current?.();\n detach.current = null;\n streamRef.current?.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }, []);\n\n const reset = useCallback((): void => {\n setStream(null);\n setSurface(null);\n setHasAudio(false);\n }, []);\n\n const stop = useCallback((): void => {\n generation.current += 1;\n release();\n reset();\n setStatus(\"idle\");\n setError(null);\n }, [release, reset]);\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 (!isScreenCaptureSupported()) {\n setError(missingCaptureApiError(\"screen\"));\n setStatus(\"error\");\n return;\n }\n\n const video: DisplayVideoConstraints = displaySurface ? { displaySurface } : {};\n const request: DisplayMediaOptionsWithHints = raw ?? {\n video,\n audio,\n ...(preferCurrentTab !== undefined ? { preferCurrentTab } : {}),\n ...(selfBrowserSurface !== undefined ? { selfBrowserSurface } : {}),\n ...(surfaceSwitching !== undefined ? { surfaceSwitching } : {}),\n ...(systemAudio !== undefined ? { systemAudio } : {}),\n };\n\n void navigator.mediaDevices\n .getDisplayMedia(request)\n .then((next) => {\n if (run !== generation.current) {\n next.getTracks().forEach((track) => track.stop());\n return;\n }\n streamRef.current = next;\n const track = next.getVideoTracks()[0];\n const ended = (): void => {\n if (run !== generation.current) return;\n release();\n reset();\n setStatus(\"idle\");\n emitEnded();\n };\n track?.addEventListener(\"ended\", ended);\n detach.current = () => track?.removeEventListener(\"ended\", ended);\n\n setStream(next);\n setSurface(track?.getSettings().displaySurface ?? null);\n setHasAudio(next.getAudioTracks().length > 0);\n setStatus(\"sharing\");\n })\n .catch((err: unknown) => {\n if (run !== generation.current) return;\n if (isCancellation(err)) {\n setStatus(\"idle\");\n emitCancelled(err);\n return;\n }\n setError(classifyMediaError(err, \"screen\"));\n setStatus(\"error\");\n });\n }, [\n audio,\n displaySurface,\n preferCurrentTab,\n selfBrowserSurface,\n surfaceSwitching,\n systemAudio,\n raw,\n release,\n reset,\n emitEnded,\n emitCancelled,\n ]);\n\n useEffect(() => release, [release]);\n\n return { status, stream, error, surface, hasAudio, supported, start, stop };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-screen-capture.cjs","names":[],"sources":["../../src/capture/use-screen-capture.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — getDisplayMedia's cancellation is indistinguishable\n * from a policy block, so the hook holds the classification, the track's own `ended`\n * event (the user can stop sharing from the browser's bar) and the stop path in one\n * place — all three end the same session.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n classifyMediaError,\n missingCaptureApiError,\n type MediaAccessError,\n} from \"@/audio/media-access\";\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\n/** Lifecycle of a screen share. */\nexport type ScreenCaptureStatus = \"idle\" | \"requesting\" | \"sharing\" | \"error\";\n\n/** Which surface to put first in the picker. */\nexport type DisplaySurfaceHint = \"monitor\" | \"window\" | \"browser\";\n\n/**\n * Hints `getDisplayMedia` takes that TypeScript's `DisplayMediaStreamOptions` does not\n * list yet.\n */\ntype DisplayMediaOptionsWithHints = DisplayMediaStreamOptions & {\n preferCurrentTab?: boolean;\n selfBrowserSurface?: \"include\" | \"exclude\";\n surfaceSwitching?: \"include\" | \"exclude\";\n systemAudio?: \"include\" | \"exclude\";\n};\n\n/** `displaySurface` is a constrainable property of a display track, but not in the DOM lib. */\ntype DisplayVideoConstraints = MediaTrackConstraints & { displaySurface?: DisplaySurfaceHint };\n\n/** Options for {@link useScreenCapture}. */\nexport interface UseScreenCaptureOptions {\n /**\n * Capture the tab's audio too. Default `false`.\n *\n * Chromium only offers this for a **tab** — sharing a window or a whole screen\n * yields no audio track no matter what you ask for, and Safari has no display\n * audio at all. Ask for it and check what you got.\n */\n audio?: boolean;\n /**\n * Which surface the picker should offer first — `\"browser\"` is a tab.\n *\n * A hint, never a guarantee: the user can always pick something else, and Firefox\n * ignores it. Read `surface` afterwards to learn what actually happened.\n */\n displaySurface?: DisplaySurfaceHint;\n /**\n * Put *this* tab at the top of the picker. Default `false`.\n *\n * The right setting for \"record what you are seeing right now\" in a support flow.\n * Chromium only.\n */\n preferCurrentTab?: boolean;\n /** Offer this tab in the list at all. `\"exclude\"` prevents the hall-of-mirrors capture. */\n selfBrowserSurface?: \"include\" | \"exclude\";\n /** Let the user switch to a different surface mid-share, without a new prompt. */\n surfaceSwitching?: \"include\" | \"exclude\";\n /** Include the system audio when a whole screen is shared. Chromium, Windows only. */\n systemAudio?: \"include\" | \"exclude\";\n /** Escape hatch: full options, replacing everything above. */\n options?: DisplayMediaStreamOptions;\n /**\n * The user stopped the share from the browser's own bar.\n *\n * The single most important callback here — see the note on\n * {@link useScreenCapture}.\n */\n onEnded?: () => void;\n /**\n * The user dismissed the picker. **Not an error.**\n *\n * Receives the rejection so an app that needs to tell a dismissal from an OS-level\n * block (macOS screen-recording permission) can look at the message. Most should\n * simply return the UI to its previous state.\n */\n onCancelled?: (reason: unknown) => void;\n}\n\n/** Value returned by {@link useScreenCapture}. */\nexport interface UseScreenCaptureResult {\n status: ScreenCaptureStatus;\n /** The live stream, or `null`. Feed it to {@link useVideoRecorder} or a `<video>`. */\n stream: MediaStream | null;\n /** Classified error, or `null`. A cancelled picker leaves this `null`. */\n error: MediaAccessError | null;\n /** What the user actually picked, when the browser reports it. */\n surface: string | null;\n /** Whether the stream carries an audio track — ask, do not assume. */\n hasAudio: boolean;\n /** `false` when `getDisplayMedia` is missing (every browser on iOS, insecure pages). */\n supported: boolean;\n /** Open the picker. Must be called from a user gesture. */\n start: () => void;\n /** Stop sharing from the app side. Fires nothing — you asked for it. */\n stop: () => void;\n}\n\n/** Whether `getDisplayMedia` is reachable at all. */\nexport function isScreenCaptureSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n navigator.mediaDevices !== undefined &&\n typeof navigator.mediaDevices.getDisplayMedia === \"function\"\n );\n}\n\n/**\n * A rejection from `getDisplayMedia` that means \"the user said no thanks\".\n *\n * There is no distinct exception for a dismissed picker: closing it produces the same\n * `NotAllowedError` as a policy block, and some builds report `AbortError` instead. The\n * useful default is therefore to treat both as a cancellation, because a\n * display-capture prompt is **always** user-initiated — nothing can open it behind\n * their back — so the overwhelmingly likely cause is that they changed their mind, and\n * a red error toast for that punishes them for it. The rejection is handed to\n * `onCancelled` so the rarer causes stay diagnosable.\n */\nfunction isCancellation(err: unknown): boolean {\n return (\n err instanceof DOMException && (err.name === \"NotAllowedError\" || err.name === \"AbortError\")\n );\n}\n\n/**\n * Capture a screen, a window or a tab with `getDisplayMedia`.\n *\n * Three states decide whether this feels right, and two of them are easy to miss:\n *\n * - **The user dismissed the picker.** A rejection, but not a failure. It leaves\n * `error` at `null` and the status back at `\"idle\"`, and calls `onCancelled`.\n * - **The user stopped the share from the browser's own bar.** Nothing in your UI was\n * clicked and no promise rejects — the *only* signal is the video track's `ended`\n * event, so the hook listens for it and clears the stream. Without that listener, an\n * app shows \"gravando\" over a stream that is already dead.\n * - **The share is live.** `surface` says what was picked and `hasAudio` says whether\n * audio actually came along, which is not what you asked for but what you got.\n *\n * The stream is owned here: `stop()` and unmount both release every track. A recorder\n * built on it (`useVideoRecorder`) deliberately does not, so stopping a recording\n * leaves the share running for the next take.\n *\n * @param options - See {@link UseScreenCaptureOptions}.\n * @returns The stream, its status, a classified error and `start`/`stop`.\n *\n * @example\n * const screen = useScreenCapture({ preferCurrentTab: true, onEnded: () => save() });\n * const rec = useVideoRecorder(screen.stream);\n * <button onClick={screen.start}>Compartilhar tela</button>\n */\nexport function useScreenCapture(options: UseScreenCaptureOptions = {}): UseScreenCaptureResult {\n const {\n audio = false,\n displaySurface,\n preferCurrentTab,\n selfBrowserSurface,\n surfaceSwitching,\n systemAudio,\n options: raw,\n onEnded,\n onCancelled,\n } = options;\n\n const [status, setStatus] = useState<ScreenCaptureStatus>(\"idle\");\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [error, setError] = useState<MediaAccessError | null>(null);\n const [surface, setSurface] = useState<string | null>(null);\n const [hasAudio, setHasAudio] = useState(false);\n const [supported] = useState(isScreenCaptureSupported);\n\n const streamRef = useRef<MediaStream | null>(null);\n const detach = useRef<(() => void) | null>(null);\n const generation = useRef(0);\n\n const emitEnded = useStableCallback(() => onEnded?.());\n const emitCancelled = useStableCallback((reason: unknown) => onCancelled?.(reason));\n\n const release = useCallback((): void => {\n detach.current?.();\n detach.current = null;\n streamRef.current?.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }, []);\n\n const reset = useCallback((): void => {\n setStream(null);\n setSurface(null);\n setHasAudio(false);\n }, []);\n\n const stop = useCallback((): void => {\n generation.current += 1;\n release();\n reset();\n setStatus(\"idle\");\n setError(null);\n }, [release, reset]);\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 (!isScreenCaptureSupported()) {\n setError(missingCaptureApiError(\"screen\"));\n setStatus(\"error\");\n return;\n }\n\n const video: DisplayVideoConstraints = displaySurface ? { displaySurface } : {};\n const request: DisplayMediaOptionsWithHints = raw ?? {\n video,\n audio,\n ...(preferCurrentTab !== undefined ? { preferCurrentTab } : {}),\n ...(selfBrowserSurface !== undefined ? { selfBrowserSurface } : {}),\n ...(surfaceSwitching !== undefined ? { surfaceSwitching } : {}),\n ...(systemAudio !== undefined ? { systemAudio } : {}),\n };\n\n void navigator.mediaDevices\n .getDisplayMedia(request)\n .then((next) => {\n if (run !== generation.current) {\n next.getTracks().forEach((track) => track.stop());\n return;\n }\n streamRef.current = next;\n const track = next.getVideoTracks()[0];\n const ended = (): void => {\n if (run !== generation.current) return;\n release();\n reset();\n setStatus(\"idle\");\n emitEnded();\n };\n track?.addEventListener(\"ended\", ended);\n detach.current = () => track?.removeEventListener(\"ended\", ended);\n\n setStream(next);\n setSurface(track?.getSettings().displaySurface ?? null);\n setHasAudio(next.getAudioTracks().length > 0);\n setStatus(\"sharing\");\n })\n .catch((err: unknown) => {\n if (run !== generation.current) return;\n if (isCancellation(err)) {\n setStatus(\"idle\");\n emitCancelled(err);\n return;\n }\n setError(classifyMediaError(err, \"screen\"));\n setStatus(\"error\");\n });\n }, [\n audio,\n displaySurface,\n preferCurrentTab,\n selfBrowserSurface,\n surfaceSwitching,\n systemAudio,\n raw,\n release,\n reset,\n emitEnded,\n emitCancelled,\n ]);\n\n useEffect(() => release, [release]);\n\n return { status, stream, error, surface, hasAudio, supported, start, stop };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-screen-capture.js","names":[],"sources":["../../src/capture/use-screen-capture.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n classifyMediaError,\n missingCaptureApiError,\n type MediaAccessError,\n} from \"@/audio/media-access\";\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\n/** Lifecycle of a screen share. */\nexport type ScreenCaptureStatus = \"idle\" | \"requesting\" | \"sharing\" | \"error\";\n\n/** Which surface to put first in the picker. */\nexport type DisplaySurfaceHint = \"monitor\" | \"window\" | \"browser\";\n\n/**\n * Hints `getDisplayMedia` takes that TypeScript's `DisplayMediaStreamOptions` does not\n * list yet.\n */\ntype DisplayMediaOptionsWithHints = DisplayMediaStreamOptions & {\n preferCurrentTab?: boolean;\n selfBrowserSurface?: \"include\" | \"exclude\";\n surfaceSwitching?: \"include\" | \"exclude\";\n systemAudio?: \"include\" | \"exclude\";\n};\n\n/** `displaySurface` is a constrainable property of a display track, but not in the DOM lib. */\ntype DisplayVideoConstraints = MediaTrackConstraints & { displaySurface?: DisplaySurfaceHint };\n\n/** Options for {@link useScreenCapture}. */\nexport interface UseScreenCaptureOptions {\n /**\n * Capture the tab's audio too. Default `false`.\n *\n * Chromium only offers this for a **tab** — sharing a window or a whole screen\n * yields no audio track no matter what you ask for, and Safari has no display\n * audio at all. Ask for it and check what you got.\n */\n audio?: boolean;\n /**\n * Which surface the picker should offer first — `\"browser\"` is a tab.\n *\n * A hint, never a guarantee: the user can always pick something else, and Firefox\n * ignores it. Read `surface` afterwards to learn what actually happened.\n */\n displaySurface?: DisplaySurfaceHint;\n /**\n * Put *this* tab at the top of the picker. Default `false`.\n *\n * The right setting for \"record what you are seeing right now\" in a support flow.\n * Chromium only.\n */\n preferCurrentTab?: boolean;\n /** Offer this tab in the list at all. `\"exclude\"` prevents the hall-of-mirrors capture. */\n selfBrowserSurface?: \"include\" | \"exclude\";\n /** Let the user switch to a different surface mid-share, without a new prompt. */\n surfaceSwitching?: \"include\" | \"exclude\";\n /** Include the system audio when a whole screen is shared. Chromium, Windows only. */\n systemAudio?: \"include\" | \"exclude\";\n /** Escape hatch: full options, replacing everything above. */\n options?: DisplayMediaStreamOptions;\n /**\n * The user stopped the share from the browser's own bar.\n *\n * The single most important callback here — see the note on\n * {@link useScreenCapture}.\n */\n onEnded?: () => void;\n /**\n * The user dismissed the picker. **Not an error.**\n *\n * Receives the rejection so an app that needs to tell a dismissal from an OS-level\n * block (macOS screen-recording permission) can look at the message. Most should\n * simply return the UI to its previous state.\n */\n onCancelled?: (reason: unknown) => void;\n}\n\n/** Value returned by {@link useScreenCapture}. */\nexport interface UseScreenCaptureResult {\n status: ScreenCaptureStatus;\n /** The live stream, or `null`. Feed it to {@link useVideoRecorder} or a `<video>`. */\n stream: MediaStream | null;\n /** Classified error, or `null`. A cancelled picker leaves this `null`. */\n error: MediaAccessError | null;\n /** What the user actually picked, when the browser reports it. */\n surface: string | null;\n /** Whether the stream carries an audio track — ask, do not assume. */\n hasAudio: boolean;\n /** `false` when `getDisplayMedia` is missing (every browser on iOS, insecure pages). */\n supported: boolean;\n /** Open the picker. Must be called from a user gesture. */\n start: () => void;\n /** Stop sharing from the app side. Fires nothing — you asked for it. */\n stop: () => void;\n}\n\n/** Whether `getDisplayMedia` is reachable at all. */\nexport function isScreenCaptureSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n navigator.mediaDevices !== undefined &&\n typeof navigator.mediaDevices.getDisplayMedia === \"function\"\n );\n}\n\n/**\n * A rejection from `getDisplayMedia` that means \"the user said no thanks\".\n *\n * There is no distinct exception for a dismissed picker: closing it produces the same\n * `NotAllowedError` as a policy block, and some builds report `AbortError` instead. The\n * useful default is therefore to treat both as a cancellation, because a\n * display-capture prompt is **always** user-initiated — nothing can open it behind\n * their back — so the overwhelmingly likely cause is that they changed their mind, and\n * a red error toast for that punishes them for it. The rejection is handed to\n * `onCancelled` so the rarer causes stay diagnosable.\n */\nfunction isCancellation(err: unknown): boolean {\n return (\n err instanceof DOMException && (err.name === \"NotAllowedError\" || err.name === \"AbortError\")\n );\n}\n\n/**\n * Capture a screen, a window or a tab with `getDisplayMedia`.\n *\n * Three states decide whether this feels right, and two of them are easy to miss:\n *\n * - **The user dismissed the picker.** A rejection, but not a failure. It leaves\n * `error` at `null` and the status back at `\"idle\"`, and calls `onCancelled`.\n * - **The user stopped the share from the browser's own bar.** Nothing in your UI was\n * clicked and no promise rejects — the *only* signal is the video track's `ended`\n * event, so the hook listens for it and clears the stream. Without that listener, an\n * app shows \"gravando\" over a stream that is already dead.\n * - **The share is live.** `surface` says what was picked and `hasAudio` says whether\n * audio actually came along, which is not what you asked for but what you got.\n *\n * The stream is owned here: `stop()` and unmount both release every track. A recorder\n * built on it (`useVideoRecorder`) deliberately does not, so stopping a recording\n * leaves the share running for the next take.\n *\n * @param options - See {@link UseScreenCaptureOptions}.\n * @returns The stream, its status, a classified error and `start`/`stop`.\n *\n * @example\n * const screen = useScreenCapture({ preferCurrentTab: true, onEnded: () => save() });\n * const rec = useVideoRecorder(screen.stream);\n * <button onClick={screen.start}>Compartilhar tela</button>\n */\nexport function useScreenCapture(options: UseScreenCaptureOptions = {}): UseScreenCaptureResult {\n const {\n audio = false,\n displaySurface,\n preferCurrentTab,\n selfBrowserSurface,\n surfaceSwitching,\n systemAudio,\n options: raw,\n onEnded,\n onCancelled,\n } = options;\n\n const [status, setStatus] = useState<ScreenCaptureStatus>(\"idle\");\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [error, setError] = useState<MediaAccessError | null>(null);\n const [surface, setSurface] = useState<string | null>(null);\n const [hasAudio, setHasAudio] = useState(false);\n const [supported] = useState(isScreenCaptureSupported);\n\n const streamRef = useRef<MediaStream | null>(null);\n const detach = useRef<(() => void) | null>(null);\n const generation = useRef(0);\n\n const emitEnded = useStableCallback(() => onEnded?.());\n const emitCancelled = useStableCallback((reason: unknown) => onCancelled?.(reason));\n\n const release = useCallback((): void => {\n detach.current?.();\n detach.current = null;\n streamRef.current?.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }, []);\n\n const reset = useCallback((): void => {\n setStream(null);\n setSurface(null);\n setHasAudio(false);\n }, []);\n\n const stop = useCallback((): void => {\n generation.current += 1;\n release();\n reset();\n setStatus(\"idle\");\n setError(null);\n }, [release, reset]);\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 (!isScreenCaptureSupported()) {\n setError(missingCaptureApiError(\"screen\"));\n setStatus(\"error\");\n return;\n }\n\n const video: DisplayVideoConstraints = displaySurface ? { displaySurface } : {};\n const request: DisplayMediaOptionsWithHints = raw ?? {\n video,\n audio,\n ...(preferCurrentTab !== undefined ? { preferCurrentTab } : {}),\n ...(selfBrowserSurface !== undefined ? { selfBrowserSurface } : {}),\n ...(surfaceSwitching !== undefined ? { surfaceSwitching } : {}),\n ...(systemAudio !== undefined ? { systemAudio } : {}),\n };\n\n void navigator.mediaDevices\n .getDisplayMedia(request)\n .then((next) => {\n if (run !== generation.current) {\n next.getTracks().forEach((track) => track.stop());\n return;\n }\n streamRef.current = next;\n const track = next.getVideoTracks()[0];\n const ended = (): void => {\n if (run !== generation.current) return;\n release();\n reset();\n setStatus(\"idle\");\n emitEnded();\n };\n track?.addEventListener(\"ended\", ended);\n detach.current = () => track?.removeEventListener(\"ended\", ended);\n\n setStream(next);\n setSurface(track?.getSettings().displaySurface ?? null);\n setHasAudio(next.getAudioTracks().length > 0);\n setStatus(\"sharing\");\n })\n .catch((err: unknown) => {\n if (run !== generation.current) return;\n if (isCancellation(err)) {\n setStatus(\"idle\");\n emitCancelled(err);\n return;\n }\n setError(classifyMediaError(err, \"screen\"));\n setStatus(\"error\");\n });\n }, [\n audio,\n displaySurface,\n preferCurrentTab,\n selfBrowserSurface,\n surfaceSwitching,\n systemAudio,\n raw,\n release,\n reset,\n emitEnded,\n emitCancelled,\n ]);\n\n useEffect(() => release, [release]);\n\n return { status, stream, error, surface, hasAudio, supported, start, stop };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-screen-capture.js","names":[],"sources":["../../src/capture/use-screen-capture.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — getDisplayMedia's cancellation is indistinguishable\n * from a policy block, so the hook holds the classification, the track's own `ended`\n * event (the user can stop sharing from the browser's bar) and the stop path in one\n * place — all three end the same session.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n classifyMediaError,\n missingCaptureApiError,\n type MediaAccessError,\n} from \"@/audio/media-access\";\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\n/** Lifecycle of a screen share. */\nexport type ScreenCaptureStatus = \"idle\" | \"requesting\" | \"sharing\" | \"error\";\n\n/** Which surface to put first in the picker. */\nexport type DisplaySurfaceHint = \"monitor\" | \"window\" | \"browser\";\n\n/**\n * Hints `getDisplayMedia` takes that TypeScript's `DisplayMediaStreamOptions` does not\n * list yet.\n */\ntype DisplayMediaOptionsWithHints = DisplayMediaStreamOptions & {\n preferCurrentTab?: boolean;\n selfBrowserSurface?: \"include\" | \"exclude\";\n surfaceSwitching?: \"include\" | \"exclude\";\n systemAudio?: \"include\" | \"exclude\";\n};\n\n/** `displaySurface` is a constrainable property of a display track, but not in the DOM lib. */\ntype DisplayVideoConstraints = MediaTrackConstraints & { displaySurface?: DisplaySurfaceHint };\n\n/** Options for {@link useScreenCapture}. */\nexport interface UseScreenCaptureOptions {\n /**\n * Capture the tab's audio too. Default `false`.\n *\n * Chromium only offers this for a **tab** — sharing a window or a whole screen\n * yields no audio track no matter what you ask for, and Safari has no display\n * audio at all. Ask for it and check what you got.\n */\n audio?: boolean;\n /**\n * Which surface the picker should offer first — `\"browser\"` is a tab.\n *\n * A hint, never a guarantee: the user can always pick something else, and Firefox\n * ignores it. Read `surface` afterwards to learn what actually happened.\n */\n displaySurface?: DisplaySurfaceHint;\n /**\n * Put *this* tab at the top of the picker. Default `false`.\n *\n * The right setting for \"record what you are seeing right now\" in a support flow.\n * Chromium only.\n */\n preferCurrentTab?: boolean;\n /** Offer this tab in the list at all. `\"exclude\"` prevents the hall-of-mirrors capture. */\n selfBrowserSurface?: \"include\" | \"exclude\";\n /** Let the user switch to a different surface mid-share, without a new prompt. */\n surfaceSwitching?: \"include\" | \"exclude\";\n /** Include the system audio when a whole screen is shared. Chromium, Windows only. */\n systemAudio?: \"include\" | \"exclude\";\n /** Escape hatch: full options, replacing everything above. */\n options?: DisplayMediaStreamOptions;\n /**\n * The user stopped the share from the browser's own bar.\n *\n * The single most important callback here — see the note on\n * {@link useScreenCapture}.\n */\n onEnded?: () => void;\n /**\n * The user dismissed the picker. **Not an error.**\n *\n * Receives the rejection so an app that needs to tell a dismissal from an OS-level\n * block (macOS screen-recording permission) can look at the message. Most should\n * simply return the UI to its previous state.\n */\n onCancelled?: (reason: unknown) => void;\n}\n\n/** Value returned by {@link useScreenCapture}. */\nexport interface UseScreenCaptureResult {\n status: ScreenCaptureStatus;\n /** The live stream, or `null`. Feed it to {@link useVideoRecorder} or a `<video>`. */\n stream: MediaStream | null;\n /** Classified error, or `null`. A cancelled picker leaves this `null`. */\n error: MediaAccessError | null;\n /** What the user actually picked, when the browser reports it. */\n surface: string | null;\n /** Whether the stream carries an audio track — ask, do not assume. */\n hasAudio: boolean;\n /** `false` when `getDisplayMedia` is missing (every browser on iOS, insecure pages). */\n supported: boolean;\n /** Open the picker. Must be called from a user gesture. */\n start: () => void;\n /** Stop sharing from the app side. Fires nothing — you asked for it. */\n stop: () => void;\n}\n\n/** Whether `getDisplayMedia` is reachable at all. */\nexport function isScreenCaptureSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n navigator.mediaDevices !== undefined &&\n typeof navigator.mediaDevices.getDisplayMedia === \"function\"\n );\n}\n\n/**\n * A rejection from `getDisplayMedia` that means \"the user said no thanks\".\n *\n * There is no distinct exception for a dismissed picker: closing it produces the same\n * `NotAllowedError` as a policy block, and some builds report `AbortError` instead. The\n * useful default is therefore to treat both as a cancellation, because a\n * display-capture prompt is **always** user-initiated — nothing can open it behind\n * their back — so the overwhelmingly likely cause is that they changed their mind, and\n * a red error toast for that punishes them for it. The rejection is handed to\n * `onCancelled` so the rarer causes stay diagnosable.\n */\nfunction isCancellation(err: unknown): boolean {\n return (\n err instanceof DOMException && (err.name === \"NotAllowedError\" || err.name === \"AbortError\")\n );\n}\n\n/**\n * Capture a screen, a window or a tab with `getDisplayMedia`.\n *\n * Three states decide whether this feels right, and two of them are easy to miss:\n *\n * - **The user dismissed the picker.** A rejection, but not a failure. It leaves\n * `error` at `null` and the status back at `\"idle\"`, and calls `onCancelled`.\n * - **The user stopped the share from the browser's own bar.** Nothing in your UI was\n * clicked and no promise rejects — the *only* signal is the video track's `ended`\n * event, so the hook listens for it and clears the stream. Without that listener, an\n * app shows \"gravando\" over a stream that is already dead.\n * - **The share is live.** `surface` says what was picked and `hasAudio` says whether\n * audio actually came along, which is not what you asked for but what you got.\n *\n * The stream is owned here: `stop()` and unmount both release every track. A recorder\n * built on it (`useVideoRecorder`) deliberately does not, so stopping a recording\n * leaves the share running for the next take.\n *\n * @param options - See {@link UseScreenCaptureOptions}.\n * @returns The stream, its status, a classified error and `start`/`stop`.\n *\n * @example\n * const screen = useScreenCapture({ preferCurrentTab: true, onEnded: () => save() });\n * const rec = useVideoRecorder(screen.stream);\n * <button onClick={screen.start}>Compartilhar tela</button>\n */\nexport function useScreenCapture(options: UseScreenCaptureOptions = {}): UseScreenCaptureResult {\n const {\n audio = false,\n displaySurface,\n preferCurrentTab,\n selfBrowserSurface,\n surfaceSwitching,\n systemAudio,\n options: raw,\n onEnded,\n onCancelled,\n } = options;\n\n const [status, setStatus] = useState<ScreenCaptureStatus>(\"idle\");\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [error, setError] = useState<MediaAccessError | null>(null);\n const [surface, setSurface] = useState<string | null>(null);\n const [hasAudio, setHasAudio] = useState(false);\n const [supported] = useState(isScreenCaptureSupported);\n\n const streamRef = useRef<MediaStream | null>(null);\n const detach = useRef<(() => void) | null>(null);\n const generation = useRef(0);\n\n const emitEnded = useStableCallback(() => onEnded?.());\n const emitCancelled = useStableCallback((reason: unknown) => onCancelled?.(reason));\n\n const release = useCallback((): void => {\n detach.current?.();\n detach.current = null;\n streamRef.current?.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }, []);\n\n const reset = useCallback((): void => {\n setStream(null);\n setSurface(null);\n setHasAudio(false);\n }, []);\n\n const stop = useCallback((): void => {\n generation.current += 1;\n release();\n reset();\n setStatus(\"idle\");\n setError(null);\n }, [release, reset]);\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 (!isScreenCaptureSupported()) {\n setError(missingCaptureApiError(\"screen\"));\n setStatus(\"error\");\n return;\n }\n\n const video: DisplayVideoConstraints = displaySurface ? { displaySurface } : {};\n const request: DisplayMediaOptionsWithHints = raw ?? {\n video,\n audio,\n ...(preferCurrentTab !== undefined ? { preferCurrentTab } : {}),\n ...(selfBrowserSurface !== undefined ? { selfBrowserSurface } : {}),\n ...(surfaceSwitching !== undefined ? { surfaceSwitching } : {}),\n ...(systemAudio !== undefined ? { systemAudio } : {}),\n };\n\n void navigator.mediaDevices\n .getDisplayMedia(request)\n .then((next) => {\n if (run !== generation.current) {\n next.getTracks().forEach((track) => track.stop());\n return;\n }\n streamRef.current = next;\n const track = next.getVideoTracks()[0];\n const ended = (): void => {\n if (run !== generation.current) return;\n release();\n reset();\n setStatus(\"idle\");\n emitEnded();\n };\n track?.addEventListener(\"ended\", ended);\n detach.current = () => track?.removeEventListener(\"ended\", ended);\n\n setStream(next);\n setSurface(track?.getSettings().displaySurface ?? null);\n setHasAudio(next.getAudioTracks().length > 0);\n setStatus(\"sharing\");\n })\n .catch((err: unknown) => {\n if (run !== generation.current) return;\n if (isCancellation(err)) {\n setStatus(\"idle\");\n emitCancelled(err);\n return;\n }\n setError(classifyMediaError(err, \"screen\"));\n setStatus(\"error\");\n });\n }, [\n audio,\n displaySurface,\n preferCurrentTab,\n selfBrowserSurface,\n surfaceSwitching,\n systemAudio,\n raw,\n release,\n reset,\n emitEnded,\n emitCancelled,\n ]);\n\n useEffect(() => release, [release]);\n\n return { status, stream, error, surface, hasAudio, supported, start, stop };\n}\n"],"mappings":"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"}