tempest-react-sdk 0.53.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -6
- package/dist/access/Can.cjs +1 -1
- package/dist/access/Can.cjs.map +1 -1
- package/dist/access/Can.js +3 -3
- package/dist/access/Can.js.map +1 -1
- package/dist/audio/level-meter.cjs +1 -1
- package/dist/audio/level-meter.cjs.map +1 -1
- package/dist/audio/level-meter.js +15 -15
- package/dist/audio/level-meter.js.map +1 -1
- package/dist/audio/noise-floor.cjs +2 -0
- package/dist/audio/noise-floor.cjs.map +1 -0
- package/dist/audio/noise-floor.js +27 -0
- package/dist/audio/noise-floor.js.map +1 -0
- package/dist/audio/voice-activity.cjs +2 -0
- package/dist/audio/voice-activity.cjs.map +1 -0
- package/dist/audio/voice-activity.js +26 -0
- package/dist/audio/voice-activity.js.map +1 -0
- package/dist/audio/voice-chain-stages.cjs +2 -0
- package/dist/audio/voice-chain-stages.cjs.map +1 -0
- package/dist/audio/voice-chain-stages.js +82 -0
- package/dist/audio/voice-chain-stages.js.map +1 -0
- package/dist/audio/voice-chain.cjs +2 -0
- package/dist/audio/voice-chain.cjs.map +1 -0
- package/dist/audio/voice-chain.js +74 -0
- package/dist/audio/voice-chain.js.map +1 -0
- package/dist/auth/lazy-with-retry.cjs.map +1 -1
- package/dist/auth/lazy-with-retry.js.map +1 -1
- package/dist/auth/passkey.cjs.map +1 -1
- package/dist/auth/passkey.js.map +1 -1
- package/dist/br/BrazilStateCitySelect.cjs +1 -1
- package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
- package/dist/br/BrazilStateCitySelect.js +36 -28
- package/dist/br/BrazilStateCitySelect.js.map +1 -1
- package/dist/br/data/br-centroids.cjs +1 -1
- package/dist/br/data/br-centroids.cjs.map +1 -1
- package/dist/br/data/br-centroids.js +11105 -11045
- package/dist/br/data/br-centroids.js.map +1 -1
- package/dist/br/data/br-locations.cjs +1 -1
- package/dist/br/data/br-locations.cjs.map +1 -1
- package/dist/br/data/br-locations.js +5803 -137
- package/dist/br/data/br-locations.js.map +1 -1
- package/dist/br/data/br-uf-geo.cjs +1 -1
- package/dist/br/data/br-uf-geo.cjs.map +1 -1
- package/dist/br/data/br-uf-geo.js +5721 -6777
- package/dist/br/data/br-uf-geo.js.map +1 -1
- package/dist/br/data/mun/AC.cjs +1 -1
- package/dist/br/data/mun/AC.cjs.map +1 -1
- package/dist/br/data/mun/AC.js +681 -689
- package/dist/br/data/mun/AC.js.map +1 -1
- package/dist/br/data/mun/AL.cjs +1 -1
- package/dist/br/data/mun/AL.cjs.map +1 -1
- package/dist/br/data/mun/AL.js +949 -844
- package/dist/br/data/mun/AL.js.map +1 -1
- package/dist/br/data/mun/AM.cjs +1 -1
- package/dist/br/data/mun/AM.cjs.map +1 -1
- package/dist/br/data/mun/AM.js +4337 -4218
- package/dist/br/data/mun/AM.js.map +1 -1
- package/dist/br/data/mun/AP.cjs +1 -1
- package/dist/br/data/mun/AP.cjs.map +1 -1
- package/dist/br/data/mun/AP.js +786 -780
- package/dist/br/data/mun/AP.js.map +1 -1
- package/dist/br/data/mun/BA.cjs +1 -1
- package/dist/br/data/mun/BA.cjs.map +1 -1
- package/dist/br/data/mun/BA.js +6329 -5633
- package/dist/br/data/mun/BA.js.map +1 -1
- package/dist/br/data/mun/CE.cjs +1 -1
- package/dist/br/data/mun/CE.cjs.map +1 -1
- package/dist/br/data/mun/CE.js +2544 -2487
- package/dist/br/data/mun/CE.js.map +1 -1
- package/dist/br/data/mun/DF.cjs +1 -1
- package/dist/br/data/mun/DF.cjs.map +1 -1
- package/dist/br/data/mun/DF.js +17 -16
- package/dist/br/data/mun/DF.js.map +1 -1
- package/dist/br/data/mun/ES.cjs +1 -1
- package/dist/br/data/mun/ES.cjs.map +1 -1
- package/dist/br/data/mun/ES.js +1101 -1070
- package/dist/br/data/mun/ES.js.map +1 -1
- package/dist/br/data/mun/GO.cjs +1 -1
- package/dist/br/data/mun/GO.cjs.map +1 -1
- package/dist/br/data/mun/GO.js +4632 -4636
- package/dist/br/data/mun/GO.js.map +1 -1
- package/dist/br/data/mun/MA.cjs +1 -1
- package/dist/br/data/mun/MA.cjs.map +1 -1
- package/dist/br/data/mun/MA.js +3603 -3457
- package/dist/br/data/mun/MA.js.map +1 -1
- package/dist/br/data/mun/MG.cjs +1 -1
- package/dist/br/data/mun/MG.cjs.map +1 -1
- package/dist/br/data/mun/MG.js +12716 -11739
- package/dist/br/data/mun/MG.js.map +1 -1
- package/dist/br/data/mun/MS.cjs +1 -1
- package/dist/br/data/mun/MS.cjs.map +1 -1
- package/dist/br/data/mun/MS.js +2182 -2335
- package/dist/br/data/mun/MS.js.map +1 -1
- package/dist/br/data/mun/MT.cjs +1 -1
- package/dist/br/data/mun/MT.cjs.map +1 -1
- package/dist/br/data/mun/MT.js +4808 -4896
- package/dist/br/data/mun/MT.js.map +1 -1
- package/dist/br/data/mun/PA.cjs +1 -1
- package/dist/br/data/mun/PA.cjs.map +1 -1
- package/dist/br/data/mun/PA.js +4981 -4901
- package/dist/br/data/mun/PA.js.map +1 -1
- package/dist/br/data/mun/PB.cjs +1 -1
- package/dist/br/data/mun/PB.cjs.map +1 -1
- package/dist/br/data/mun/PB.js +2418 -1956
- package/dist/br/data/mun/PB.js.map +1 -1
- package/dist/br/data/mun/PE.cjs +1 -1
- package/dist/br/data/mun/PE.cjs.map +1 -1
- package/dist/br/data/mun/PE.js +2164 -1987
- package/dist/br/data/mun/PE.js.map +1 -1
- package/dist/br/data/mun/PI.cjs +1 -1
- package/dist/br/data/mun/PI.cjs.map +1 -1
- package/dist/br/data/mun/PI.js +2982 -2844
- package/dist/br/data/mun/PI.js.map +1 -1
- package/dist/br/data/mun/PR.cjs +1 -1
- package/dist/br/data/mun/PR.cjs.map +1 -1
- package/dist/br/data/mun/PR.js +5425 -5240
- package/dist/br/data/mun/PR.js.map +1 -1
- package/dist/br/data/mun/RJ.cjs +1 -1
- package/dist/br/data/mun/RJ.cjs.map +1 -1
- package/dist/br/data/mun/RJ.js +1395 -1178
- package/dist/br/data/mun/RJ.js.map +1 -1
- package/dist/br/data/mun/RN.cjs +1 -1
- package/dist/br/data/mun/RN.cjs.map +1 -1
- package/dist/br/data/mun/RN.js +1608 -1382
- package/dist/br/data/mun/RN.js.map +1 -1
- package/dist/br/data/mun/RO.cjs +1 -1
- package/dist/br/data/mun/RO.cjs.map +1 -1
- package/dist/br/data/mun/RO.js +1392 -1470
- package/dist/br/data/mun/RO.js.map +1 -1
- package/dist/br/data/mun/RR.cjs +1 -1
- package/dist/br/data/mun/RR.cjs.map +1 -1
- package/dist/br/data/mun/RR.js +795 -778
- package/dist/br/data/mun/RR.js.map +1 -1
- package/dist/br/data/mun/RS.cjs +1 -1
- package/dist/br/data/mun/RS.cjs.map +1 -1
- package/dist/br/data/mun/RS.js +7579 -6424
- package/dist/br/data/mun/RS.js.map +1 -1
- package/dist/br/data/mun/SC.cjs +1 -1
- package/dist/br/data/mun/SC.cjs.map +1 -1
- package/dist/br/data/mun/SC.js +3979 -3357
- package/dist/br/data/mun/SC.js.map +1 -1
- package/dist/br/data/mun/SE.cjs +1 -1
- package/dist/br/data/mun/SE.cjs.map +1 -1
- package/dist/br/data/mun/SE.js +812 -625
- package/dist/br/data/mun/SE.js.map +1 -1
- package/dist/br/data/mun/SP.cjs +1 -1
- package/dist/br/data/mun/SP.cjs.map +1 -1
- package/dist/br/data/mun/SP.js +7866 -6803
- package/dist/br/data/mun/SP.js.map +1 -1
- package/dist/br/data/mun/TO.cjs +1 -1
- package/dist/br/data/mun/TO.cjs.map +1 -1
- package/dist/br/data/mun/TO.js +2763 -2817
- package/dist/br/data/mun/TO.js.map +1 -1
- package/dist/br/locations.cjs +1 -1
- package/dist/br/locations.cjs.map +1 -1
- package/dist/br/locations.js +97 -24
- package/dist/br/locations.js.map +1 -1
- package/dist/br/scales.cjs +1 -1
- package/dist/br/scales.cjs.map +1 -1
- package/dist/br/scales.js +8 -4
- package/dist/br/scales.js.map +1 -1
- package/dist/br/whatsapp.cjs +2 -0
- package/dist/br/whatsapp.cjs.map +1 -0
- package/dist/br/whatsapp.js +21 -0
- package/dist/br/whatsapp.js.map +1 -0
- package/dist/br.cjs +1 -1
- package/dist/br.cjs.map +1 -1
- package/dist/br.d.ts +188 -10
- package/dist/br.js +18 -17
- package/dist/br.js.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.cjs +1 -1
- package/dist/components/BottomSheet/BottomSheet.cjs.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.js +30 -27
- package/dist/components/BottomSheet/BottomSheet.js.map +1 -1
- package/dist/components/Command/Command.cjs +1 -1
- package/dist/components/Command/Command.cjs.map +1 -1
- package/dist/components/Command/Command.js +78 -75
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/Drawer/Drawer.cjs +1 -1
- package/dist/components/Drawer/Drawer.cjs.map +1 -1
- package/dist/components/Drawer/Drawer.js +42 -39
- package/dist/components/Drawer/Drawer.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js +111 -67
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/Modal/Modal.cjs +1 -1
- package/dist/components/Modal/Modal.cjs.map +1 -1
- package/dist/components/Modal/Modal.js +45 -42
- package/dist/components/Modal/Modal.js.map +1 -1
- package/dist/components/Money/Money.cjs +1 -1
- package/dist/components/Money/Money.cjs.map +1 -1
- package/dist/components/Money/Money.js +10 -9
- package/dist/components/Money/Money.js.map +1 -1
- package/dist/components/Pagination/Pagination.cjs +1 -1
- package/dist/components/Pagination/Pagination.cjs.map +1 -1
- package/dist/components/Pagination/Pagination.js +38 -31
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/Pagination/Pagination.module.cjs +1 -1
- package/dist/components/Pagination/Pagination.module.cjs.map +1 -1
- package/dist/components/Pagination/Pagination.module.js +4 -4
- package/dist/components/Pagination/Pagination.module.js.map +1 -1
- package/dist/components/Portal/Portal.cjs +1 -1
- package/dist/components/Portal/Portal.cjs.map +1 -1
- package/dist/components/Portal/Portal.js +9 -6
- package/dist/components/Portal/Portal.js.map +1 -1
- package/dist/components/Portal/portal-host.cjs +2 -0
- package/dist/components/Portal/portal-host.cjs.map +1 -0
- package/dist/components/Portal/portal-host.js +11 -0
- package/dist/components/Portal/portal-host.js.map +1 -0
- package/dist/components/Scheduler/Scheduler.cjs +1 -1
- package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
- package/dist/components/Scheduler/Scheduler.js +83 -82
- package/dist/components/Scheduler/Scheduler.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js +55 -53
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/components/VirtualList/VirtualList.cjs +1 -1
- package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
- package/dist/components/VirtualList/VirtualList.js +41 -31
- package/dist/components/VirtualList/VirtualList.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.js +78 -68
- package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
- package/dist/forms/masked-inputs.cjs +1 -1
- package/dist/forms/masked-inputs.cjs.map +1 -1
- package/dist/forms/masked-inputs.js +20 -19
- package/dist/forms/masked-inputs.js.map +1 -1
- package/dist/hooks/use-fullscreen-element.cjs +2 -0
- package/dist/hooks/use-fullscreen-element.cjs.map +1 -0
- package/dist/hooks/use-fullscreen-element.js +20 -0
- package/dist/hooks/use-fullscreen-element.js.map +1 -0
- package/dist/hooks/use-fullscreen.cjs +2 -0
- package/dist/hooks/use-fullscreen.cjs.map +1 -0
- package/dist/hooks/use-fullscreen.js +66 -0
- package/dist/hooks/use-fullscreen.js.map +1 -0
- package/dist/hooks/use-push-to-talk.cjs +2 -0
- package/dist/hooks/use-push-to-talk.cjs.map +1 -0
- package/dist/hooks/use-push-to-talk.js +61 -0
- package/dist/hooks/use-push-to-talk.js.map +1 -0
- package/dist/http/describe-api-error.cjs.map +1 -1
- package/dist/http/describe-api-error.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +34 -16
- package/dist/http/errors.js.map +1 -1
- package/dist/http/parse-response.cjs +2 -2
- package/dist/http/parse-response.cjs.map +1 -1
- package/dist/http/parse-response.js +9 -8
- package/dist/http/parse-response.js.map +1 -1
- package/dist/i18n/create-i18n.cjs +1 -1
- package/dist/i18n/create-i18n.cjs.map +1 -1
- package/dist/i18n/create-i18n.js +28 -27
- package/dist/i18n/create-i18n.js.map +1 -1
- package/dist/router/AppRouter.cjs +1 -1
- package/dist/router/AppRouter.cjs.map +1 -1
- package/dist/router/AppRouter.js +28 -21
- package/dist/router/AppRouter.js.map +1 -1
- package/dist/router/RouteGuard.cjs +1 -1
- package/dist/router/RouteGuard.cjs.map +1 -1
- package/dist/router/RouteGuard.js +4 -4
- package/dist/router/RouteGuard.js.map +1 -1
- package/dist/sse/create-event-stream.cjs +1 -1
- package/dist/sse/create-event-stream.cjs.map +1 -1
- package/dist/sse/create-event-stream.js +34 -38
- package/dist/sse/create-event-stream.js.map +1 -1
- package/dist/styles/AIChat.css +79 -0
- package/dist/styles/Accordion.css +12 -0
- package/dist/styles/Alert.css +32 -0
- package/dist/styles/AppBar.css +17 -0
- package/dist/styles/AppShell.css +8 -0
- package/dist/styles/AspectRatio.css +3 -0
- package/dist/styles/AudioPlayer.css +13 -0
- package/dist/styles/AudioRecorder.css +20 -0
- package/dist/styles/Avatar.css +13 -0
- package/dist/styles/AvatarGroup.css +18 -0
- package/dist/styles/Badge.css +25 -0
- package/dist/styles/Banner.css +13 -0
- package/dist/styles/BarList.css +10 -0
- package/dist/styles/BarcodeScanner.css +21 -0
- package/dist/styles/BottomNavigation.css +10 -0
- package/dist/styles/BottomSheet.css +11 -0
- package/dist/styles/BrazilMap.css +20 -0
- package/dist/styles/Breadcrumbs.css +8 -0
- package/dist/styles/Button.css +42 -0
- package/dist/styles/Calendar.css +16 -0
- package/dist/styles/Card.css +15 -0
- package/dist/styles/Carousel.css +17 -0
- package/dist/styles/Center.css +5 -0
- package/dist/styles/Chat.css +35 -0
- package/dist/styles/Checkbox.css +11 -0
- package/dist/styles/ChipInput.css +13 -0
- package/dist/styles/CodeBlock.css +25 -0
- package/dist/styles/Collapsible.css +6 -0
- package/dist/styles/Combobox.css +18 -0
- package/dist/styles/Command.css +20 -0
- package/dist/styles/ContextMenu.css +11 -0
- package/dist/styles/CopyButton.css +4 -0
- package/dist/styles/DataList.css +3 -0
- package/dist/styles/DataTable.css +19 -0
- package/dist/styles/DateRangePicker.css +22 -0
- package/dist/styles/DescriptionList.css +4 -0
- package/dist/styles/Divider.css +11 -0
- package/dist/styles/Drawer.css +22 -0
- package/dist/styles/DropdownMenu.css +16 -0
- package/dist/styles/Dropzone.css +7 -0
- package/dist/styles/EmptyState.css +6 -0
- package/dist/styles/ErrorState.css +6 -0
- package/dist/styles/ErrorText.css +2 -0
- package/dist/styles/FileUpload.css +17 -0
- package/dist/styles/FilterBar.css +19 -0
- package/dist/styles/FloatingActionButton.css +23 -0
- package/dist/styles/Form.css +21 -0
- package/dist/styles/HoverCard.css +11 -0
- package/dist/styles/IconPicker.css +6 -0
- package/dist/styles/Image.css +2 -0
- package/dist/styles/ImageCropper.css +16 -0
- package/dist/styles/Input.css +24 -0
- package/dist/styles/InstallBanner.css +8 -0
- package/dist/styles/Kanban.css +15 -0
- package/dist/styles/Kbd.css +5 -0
- package/dist/styles/Label.css +3 -0
- package/dist/styles/Layout.css +22 -0
- package/dist/styles/Lightbox.css +21 -0
- package/dist/styles/ListTile.css +14 -0
- package/dist/styles/MapLegend.css +9 -0
- package/dist/styles/Markdown.css +24 -0
- package/dist/styles/Masonry.css +4 -0
- package/dist/styles/Menubar.css +17 -0
- package/dist/styles/Modal.css +21 -0
- package/dist/styles/ModalsManager.css +2 -0
- package/dist/styles/MultiSelect.css +21 -0
- package/dist/styles/MunicipalitySearch.css +6 -0
- package/dist/styles/NProgress.css +2 -0
- package/dist/styles/Navbar.css +10 -0
- package/dist/styles/NavigationMenu.css +12 -0
- package/dist/styles/NavigationRail.css +14 -0
- package/dist/styles/NotificationCenter.css +21 -0
- package/dist/styles/OfflineIndicator.css +7 -0
- package/dist/styles/Page.css +13 -0
- package/dist/styles/Pagination.css +12 -0
- package/dist/styles/PasswordInput.css +21 -0
- package/dist/styles/PinInput.css +13 -0
- package/dist/styles/PixQRCode.css +9 -0
- package/dist/styles/Popover.css +10 -0
- package/dist/styles/Progress.css +11 -0
- package/dist/styles/QRCode.css +3 -0
- package/dist/styles/Radio.css +14 -0
- package/dist/styles/RangeSlider.css +15 -0
- package/dist/styles/RatingStars.css +11 -0
- package/dist/styles/RefreshIndicator.css +5 -0
- package/dist/styles/Resizable.css +13 -0
- package/dist/styles/RichTextEditor.css +19 -0
- package/dist/styles/SafeArea.css +7 -0
- package/dist/styles/Scheduler.css +24 -0
- package/dist/styles/ScrollArea.css +7 -0
- package/dist/styles/SearchBar.css +11 -0
- package/dist/styles/SegmentedControl.css +12 -0
- package/dist/styles/Select.css +14 -0
- package/dist/styles/Sidebar.css +19 -0
- package/dist/styles/SignaturePad.css +6 -0
- package/dist/styles/Skeleton.css +7 -0
- package/dist/styles/Slider.css +15 -0
- package/dist/styles/Spacer.css +4 -0
- package/dist/styles/Sparkline.css +4 -0
- package/dist/styles/Spinner.css +12 -0
- package/dist/styles/Stat.css +12 -0
- package/dist/styles/Stepper.css +17 -0
- package/dist/styles/StepperInput.css +15 -0
- package/dist/styles/Switch.css +11 -0
- package/dist/styles/SyncStatusBadge.css +13 -0
- package/dist/styles/Table.css +16 -0
- package/dist/styles/Tabs.css +14 -0
- package/dist/styles/Tag.css +14 -0
- package/dist/styles/Textarea.css +13 -0
- package/dist/styles/TimePicker.css +14 -0
- package/dist/styles/Timeline.css +17 -0
- package/dist/styles/Toast.css +21 -0
- package/dist/styles/Toggle.css +12 -0
- package/dist/styles/ToggleGroup.css +9 -0
- package/dist/styles/Tooltip.css +10 -0
- package/dist/styles/Tour.css +20 -0
- package/dist/styles/TrajectoryMap.css +16 -0
- package/dist/styles/Transfer.css +21 -0
- package/dist/styles/TreeView.css +17 -0
- package/dist/styles/TruncateText.css +2 -0
- package/dist/styles/UpdatePrompt.css +10 -0
- package/dist/styles/VirtualList.css +5 -0
- package/dist/styles/VirtualTable.css +17 -0
- package/dist/styles/VisuallyHidden.css +2 -0
- package/dist/styles/Wizard.css +4 -0
- package/dist/styles/actions.css +104 -0
- package/dist/styles/advanced.css +66 -0
- package/dist/styles/br.css +41 -0
- package/dist/styles/chat.css +113 -0
- package/dist/styles/core.css +40 -0
- package/dist/styles/data.css +222 -0
- package/dist/styles/editor.css +19 -0
- package/dist/styles/feedback.css +201 -0
- package/dist/styles/forms.css +321 -0
- package/dist/styles/geo.css +16 -0
- package/dist/styles/icons.css +6 -0
- package/dist/styles/identity.css +48 -0
- package/dist/styles/layout.css +122 -0
- package/dist/styles/media.css +52 -0
- package/dist/styles/navigation.css +167 -0
- package/dist/styles/overlay.css +73 -0
- package/dist/styles/utility.css +39 -0
- package/dist/styles.css +1 -1
- package/dist/sw/register-service-worker.cjs +1 -1
- package/dist/sw/register-service-worker.cjs.map +1 -1
- package/dist/sw/register-service-worker.js +21 -17
- package/dist/sw/register-service-worker.js.map +1 -1
- package/dist/sw.d.ts +17 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +1151 -41
- package/dist/tempest-react-sdk.js +295 -286
- package/dist/utils/dev-mode.cjs.map +1 -1
- package/dist/utils/dev-mode.js.map +1 -1
- package/dist/utils/format.cjs +1 -1
- package/dist/utils/format.cjs.map +1 -1
- package/dist/utils/format.js +22 -14
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/intl-cache.cjs +2 -0
- package/dist/utils/intl-cache.cjs.map +1 -0
- package/dist/utils/intl-cache.js +23 -0
- package/dist/utils/intl-cache.js.map +1 -0
- package/dist/utils/json-frame.cjs +2 -0
- package/dist/utils/json-frame.cjs.map +1 -0
- package/dist/utils/json-frame.js +30 -0
- package/dist/utils/json-frame.js.map +1 -0
- package/dist/utils/numbers.cjs +1 -1
- package/dist/utils/numbers.cjs.map +1 -1
- package/dist/utils/numbers.js +8 -7
- package/dist/utils/numbers.js.map +1 -1
- package/dist/vision/results.cjs +1 -1
- package/dist/vision/results.cjs.map +1 -1
- package/dist/vision/results.js +26 -17
- package/dist/vision/results.js.map +1 -1
- package/dist/vision.d.ts +29 -0
- package/dist/vite/tempest-icons.cjs +1 -1
- package/dist/vite/tempest-icons.cjs.map +1 -1
- package/dist/vite/tempest-icons.js +1 -1
- package/dist/vite/tempest-icons.js.map +1 -1
- package/dist/webrtc/link-stats.cjs +2 -0
- package/dist/webrtc/link-stats.cjs.map +1 -0
- package/dist/webrtc/link-stats.js +74 -0
- package/dist/webrtc/link-stats.js.map +1 -0
- package/dist/webrtc/mesh-quality.cjs +2 -0
- package/dist/webrtc/mesh-quality.cjs.map +1 -0
- package/dist/webrtc/mesh-quality.js +51 -0
- package/dist/webrtc/mesh-quality.js.map +1 -0
- package/dist/webrtc/peer-link.cjs +2 -0
- package/dist/webrtc/peer-link.cjs.map +1 -0
- package/dist/webrtc/peer-link.js +45 -0
- package/dist/webrtc/peer-link.js.map +1 -0
- package/dist/webrtc/peer-mesh.cjs +2 -0
- package/dist/webrtc/peer-mesh.cjs.map +1 -0
- package/dist/webrtc/peer-mesh.js +155 -0
- package/dist/webrtc/peer-mesh.js.map +1 -0
- package/dist/webrtc/use-link-stats.cjs +2 -0
- package/dist/webrtc/use-link-stats.cjs.map +1 -0
- package/dist/webrtc/use-link-stats.js +22 -0
- package/dist/webrtc/use-link-stats.js.map +1 -0
- package/dist/ws/create-web-socket.cjs +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js +81 -85
- package/dist/ws/create-web-socket.js.map +1 -1
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +42 -48
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +5 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"results.js","names":[],"sources":["../../src/vision/results.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Per-image result envelopes — Ultralytics-style `Results` for the SDK.\n *\n * Each `predict()` call returns a 1-element array of these envelopes,\n * mirroring `YOLO(\"img.jpg\")`. Each envelope holds:\n *\n * - A bulk-array view of the predictions (`Boxes`, `Probs`, `Masks`) with\n * the exact attribute names Ultralytics uses (`xyxy`, `xywh`, `xyxyn`,\n * `xywhn`, `cls`, `conf`, `data`, `top1`, `top5`).\n * - Per-instance dataclasses (`DetectionResult`, `SegmentationResult`,\n * `ClassProbability`) for callers who prefer the OO interface.\n * - `names`: `Record<number, string>` matching Ultralytics' `model.names`.\n * - `origImg` / `origShape` / `path`: provenance for the original input.\n * - `speed`: per-stage timings of the `predict()` call that produced it.\n */\n\nimport { type Speed } from \"./core/timing\";\nimport {\n type ClassificationResult,\n type DetectionResult,\n type RGBImage,\n type SegmentationResult,\n} from \"./types\";\n\n/**\n * Zero timings for an envelope built outside a `predict()` call.\n *\n * Hand-constructed envelopes (tests, adapters) have nothing to measure, and\n * zeros keep `speed.inference` a plain `number` at every call site instead of\n * forcing an optional check that only ever fires for synthetic data.\n */\nconst NO_SPEED: Speed = { load: 0, preprocess: 0, inference: 0, postprocess: 0 };\n\n/**\n * Bulk numpy-style view of detected boxes for a single image.\n *\n * Mirrors Ultralytics' `Boxes` interface. Coordinates in {@link xyxy} and\n * {@link xywh} are absolute pixels in the original image; the `*n` variants\n * are normalized to `[0, 1]` using `origShape`.\n */\nexport class Boxes {\n /**\n * @param xyxy Flat array of length `4 * N` in `[x1, y1, x2, y2, ...]` order.\n * @param cls One class index per box, length `N`.\n * @param conf One confidence per box, length `N`.\n * @param origShape `[height, width]` of the original image.\n */\n constructor(\n public readonly xyxy: Float32Array,\n public readonly cls: Int32Array,\n public readonly conf: Float32Array,\n public readonly origShape: readonly [number, number],\n ) {}\n\n /** Number of detected boxes. */\n get length(): number {\n return this.cls.length;\n }\n\n /** `[N, 4]` shape of the `xyxy` view. */\n get shape(): readonly [number, number] {\n return [this.length, 4];\n }\n\n /** Boxes as `[N, 4]` `[cx, cy, w, h]` flat array in absolute pixels. */\n get xywh(): Float32Array {\n const out = new Float32Array(this.xyxy.length);\n for (let i = 0; i < this.length; i++) {\n const x1 = this.xyxy[i * 4] as number;\n const y1 = this.xyxy[i * 4 + 1] as number;\n const x2 = this.xyxy[i * 4 + 2] as number;\n const y2 = this.xyxy[i * 4 + 3] as number;\n out[i * 4] = (x1 + x2) / 2;\n out[i * 4 + 1] = (y1 + y2) / 2;\n out[i * 4 + 2] = x2 - x1;\n out[i * 4 + 3] = y2 - y1;\n }\n return out;\n }\n\n /** Boxes as `[N, 4]` `[x1, y1, x2, y2]` normalized to `[0, 1]`. */\n get xyxyn(): Float32Array {\n const [h, w] = this.origShape;\n const out = new Float32Array(this.xyxy.length);\n if (this.length === 0 || w <= 0 || h <= 0) return out;\n for (let i = 0; i < this.length; i++) {\n out[i * 4] = (this.xyxy[i * 4] as number) / w;\n out[i * 4 + 1] = (this.xyxy[i * 4 + 1] as number) / h;\n out[i * 4 + 2] = (this.xyxy[i * 4 + 2] as number) / w;\n out[i * 4 + 3] = (this.xyxy[i * 4 + 3] as number) / h;\n }\n return out;\n }\n\n /** Boxes as `[N, 4]` `[cx, cy, w, h]` normalized to `[0, 1]`. */\n get xywhn(): Float32Array {\n const xywh = this.xywh;\n const [h, w] = this.origShape;\n if (this.length === 0 || w <= 0 || h <= 0) return xywh;\n for (let i = 0; i < this.length; i++) {\n xywh[i * 4] = (xywh[i * 4] as number) / w;\n xywh[i * 4 + 1] = (xywh[i * 4 + 1] as number) / h;\n xywh[i * 4 + 2] = (xywh[i * 4 + 2] as number) / w;\n xywh[i * 4 + 3] = (xywh[i * 4 + 3] as number) / h;\n }\n return xywh;\n }\n\n /**\n * Concatenated `[N, 6]` array of `[x1, y1, x2, y2, conf, cls]`.\n *\n * Matches Ultralytics' `boxes.data`.\n */\n get data(): Float32Array {\n const out = new Float32Array(this.length * 6);\n for (let i = 0; i < this.length; i++) {\n out[i * 6] = this.xyxy[i * 4] as number;\n out[i * 6 + 1] = this.xyxy[i * 4 + 1] as number;\n out[i * 6 + 2] = this.xyxy[i * 4 + 2] as number;\n out[i * 6 + 3] = this.xyxy[i * 4 + 3] as number;\n out[i * 6 + 4] = this.conf[i] as number;\n out[i * 6 + 5] = this.cls[i] as number;\n }\n return out;\n }\n}\n\n/**\n * Top-k classification probabilities for a single image.\n *\n * Mirrors Ultralytics' `Probs` interface.\n */\nexport class Probs {\n /** @param data `[numClasses]` per-class probabilities, indexed by class id. */\n constructor(public readonly data: Float32Array) {}\n\n /** Number of classes. */\n get length(): number {\n return this.data.length;\n }\n\n /** `[numClasses]` shape of the underlying vector. */\n get shape(): readonly [number] {\n return [this.length];\n }\n\n /** Index of the most probable class. */\n get top1(): number {\n if (this.data.length === 0) return 0;\n let best = 0;\n let bestVal = this.data[0] as number;\n for (let i = 1; i < this.data.length; i++) {\n const v = this.data[i] as number;\n if (v > bestVal) {\n best = i;\n bestVal = v;\n }\n }\n return best;\n }\n\n /** Probability of the top-1 class. */\n get top1conf(): number {\n if (this.data.length === 0) return 0;\n return this.data[this.top1] as number;\n }\n\n /** Indices of the top-5 most probable classes, descending. */\n get top5(): Int32Array {\n return this._topK(5).indices;\n }\n\n /** Probabilities of the top-5 classes, descending. */\n get top5conf(): Float32Array {\n return this._topK(5).values;\n }\n\n private _topK(k: number): { indices: Int32Array; values: Float32Array } {\n const n = Math.min(k, this.data.length);\n const order: number[] = [];\n for (let i = 0; i < this.data.length; i++) order.push(i);\n order.sort((a, b) => (this.data[b] as number) - (this.data[a] as number));\n const indices = new Int32Array(n);\n const values = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n indices[i] = order[i] as number;\n values[i] = this.data[order[i] as number] as number;\n }\n return { indices, values };\n }\n}\n\n/**\n * Per-instance binary masks for a single image.\n *\n * Each mask is cropped to its instance's bounding box. To paint masks onto\n * a full-image canvas, use `xyxy[i]` as the top-left target.\n */\nexport class Masks {\n /**\n * @param data Per-instance binary masks (`Mask` objects from `types.ts`).\n * @param xyxy Flat `[N, 4]` of bounding-box coordinates in original pixels.\n * @param origShape `[height, width]` of the original image.\n */\n constructor(\n public readonly data: ReadonlyArray<{\n readonly data: Uint8Array;\n readonly width: number;\n readonly height: number;\n }>,\n public readonly xyxy: Float32Array,\n public readonly origShape: readonly [number, number],\n ) {}\n\n /** Number of instance masks. */\n get length(): number {\n return this.data.length;\n }\n\n /** `[N]` shape of the masks collection. */\n get shape(): readonly [number] {\n return [this.length];\n }\n\n [Symbol.iterator](): Iterator<{\n readonly data: Uint8Array;\n readonly width: number;\n readonly height: number;\n }> {\n return this.data[Symbol.iterator]();\n }\n}\n\n/**\n * Per-image detection envelope (Ultralytics-style `Results`).\n *\n * Iterating yields per-instance {@link DetectionResult} entries, so legacy\n * code that did `for (const d of detector.predict(img))` only needs an\n * extra `[0]` to bridge:\n *\n * ```typescript\n * for (const d of (await detector.predict(img))[0]) {\n * console.log(d.cls, d.conf, d.box.xyxy);\n * }\n * ```\n *\n * For numpy-style bulk access, use the `boxes` collection.\n */\nexport class DetectionResults implements Iterable<DetectionResult> {\n constructor(\n public readonly boxes: Boxes,\n public readonly detections: readonly DetectionResult[],\n public readonly names: Readonly<Record<number, string>>,\n public readonly origImg: RGBImage,\n public readonly origShape: readonly [number, number],\n public readonly path: string | null = null,\n public readonly speed: Readonly<Speed> = NO_SPEED,\n ) {}\n\n /** Number of surviving detections. */\n get length(): number {\n return this.detections.length;\n }\n\n /** Index into the per-instance detections. */\n get(index: number): DetectionResult | undefined {\n return this.detections[index];\n }\n\n [Symbol.iterator](): Iterator<DetectionResult> {\n return this.detections[Symbol.iterator]();\n }\n}\n\n/**\n * Per-image envelope for a fused detect→classify pipeline.\n *\n * Structurally a {@link DetectionResults} with a second class map: every\n * detection it yields carries a populated `classification`, and the two stages\n * have their own, unrelated label spaces — a detector that finds `sheep`\n * feeding a classifier that answers `famacha_3` shares no class ids with it.\n * Merging them into one `names` record would make `cls` and\n * `classification.cls` look comparable when they are not.\n *\n * ```typescript\n * const result = (await pipeline.predict(\"flock.jpg\"))[0];\n * for (const detection of result) {\n * console.log(detection.name, detection.conf, detection.classification?.name);\n * }\n * ```\n */\nexport class DetectClassifyResults implements Iterable<DetectionResult> {\n constructor(\n public readonly boxes: Boxes,\n public readonly detections: readonly DetectionResult[],\n public readonly names: Readonly<Record<number, string>>,\n public readonly classifierNames: Readonly<Record<number, string>>,\n public readonly origImg: RGBImage,\n public readonly origShape: readonly [number, number],\n public readonly path: string | null = null,\n public readonly speed: Readonly<Speed> = NO_SPEED,\n ) {}\n\n /** Number of surviving detections. */\n get length(): number {\n return this.detections.length;\n }\n\n /** Index into the per-instance detections. */\n get(index: number): DetectionResult | undefined {\n return this.detections[index];\n }\n\n [Symbol.iterator](): Iterator<DetectionResult> {\n return this.detections[Symbol.iterator]();\n }\n}\n\n/**\n * Per-image classification envelope (Ultralytics-style `Results`).\n */\nexport class ClassificationResults {\n constructor(\n public readonly probs: Probs,\n public readonly result: ClassificationResult,\n public readonly names: Readonly<Record<number, string>>,\n public readonly origImg: RGBImage,\n public readonly origShape: readonly [number, number],\n public readonly path: string | null = null,\n public readonly speed: Readonly<Speed> = NO_SPEED,\n ) {}\n\n /** Top-1 class index (Ultralytics-style alias). */\n get cls(): number {\n return this.probs.top1;\n }\n\n /** Top-1 confidence (Ultralytics-style alias). */\n get conf(): number {\n return this.probs.top1conf;\n }\n\n /** Top-1 class name. */\n get name(): string {\n return this.names[this.cls] ?? `class_${this.cls}`;\n }\n\n /** Per-class probability list, sorted descending (legacy field). */\n get probabilities(): readonly ClassificationResult[\"probabilities\"][number][] {\n return this.result.probabilities;\n }\n}\n\n/**\n * Per-image instance-segmentation envelope (Ultralytics-style `Results`).\n *\n * Iterating yields per-instance {@link SegmentationResult} entries. `boxes`\n * and `masks` mirror Ultralytics' bulk-array views.\n */\nexport class SegmentationResults implements Iterable<SegmentationResult> {\n constructor(\n public readonly boxes: Boxes,\n public readonly masks: Masks,\n public readonly detections: readonly SegmentationResult[],\n public readonly names: Readonly<Record<number, string>>,\n public readonly origImg: RGBImage,\n public readonly origShape: readonly [number, number],\n public readonly path: string | null = null,\n public readonly speed: Readonly<Speed> = NO_SPEED,\n ) {}\n\n /** Number of surviving instances. */\n get length(): number {\n return this.detections.length;\n }\n\n /** Index into the per-instance results. */\n get(index: number): SegmentationResult | undefined {\n return this.detections[index];\n }\n\n [Symbol.iterator](): Iterator<SegmentationResult> {\n return this.detections[Symbol.iterator]();\n }\n}\n"],"mappings":";AAgCA,IAAM,IAAkB;CAAE,MAAM;CAAG,YAAY;CAAG,WAAW;CAAG,aAAa;AAAE,GASlE,IAAb,MAAmB;CAQK;CACA;CACA;CACA;CAJpB,YACI,GACA,GACA,GACA,GACF;EADkB,AAHA,KAAA,OAAA,GACA,KAAA,MAAA,GACA,KAAA,OAAA,GACA,KAAA,YAAA;CACjB;CAGH,IAAI,SAAiB;EACjB,OAAO,KAAK,IAAI;CACpB;CAGA,IAAI,QAAmC;EACnC,OAAO,CAAC,KAAK,QAAQ,CAAC;CAC1B;CAGA,IAAI,OAAqB;EACrB,IAAM,IAAM,IAAI,aAAa,KAAK,KAAK,MAAM;EAC7C,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;GAClC,IAAM,IAAK,KAAK,KAAK,IAAI,IACnB,IAAK,KAAK,KAAK,IAAI,IAAI,IACvB,IAAK,KAAK,KAAK,IAAI,IAAI,IACvB,IAAK,KAAK,KAAK,IAAI,IAAI;GAI7B,AAHA,EAAI,IAAI,MAAM,IAAK,KAAM,GACzB,EAAI,IAAI,IAAI,MAAM,IAAK,KAAM,GAC7B,EAAI,IAAI,IAAI,KAAK,IAAK,GACtB,EAAI,IAAI,IAAI,KAAK,IAAK;EAC1B;EACA,OAAO;CACX;CAGA,IAAI,QAAsB;EACtB,IAAM,CAAC,GAAG,KAAK,KAAK,WACd,IAAM,IAAI,aAAa,KAAK,KAAK,MAAM;EAC7C,IAAI,KAAK,WAAW,KAAK,KAAK,KAAK,KAAK,GAAG,OAAO;EAClD,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAI7B,AAHA,EAAI,IAAI,KAAM,KAAK,KAAK,IAAI,KAAgB,GAC5C,EAAI,IAAI,IAAI,KAAM,KAAK,KAAK,IAAI,IAAI,KAAgB,GACpD,EAAI,IAAI,IAAI,KAAM,KAAK,KAAK,IAAI,IAAI,KAAgB,GACpD,EAAI,IAAI,IAAI,KAAM,KAAK,KAAK,IAAI,IAAI,KAAgB;EAExD,OAAO;CACX;CAGA,IAAI,QAAsB;EACtB,IAAM,IAAO,KAAK,MACZ,CAAC,GAAG,KAAK,KAAK;EACpB,IAAI,KAAK,WAAW,KAAK,KAAK,KAAK,KAAK,GAAG,OAAO;EAClD,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAI7B,AAHA,EAAK,IAAI,KAAM,EAAK,IAAI,KAAgB,GACxC,EAAK,IAAI,IAAI,KAAM,EAAK,IAAI,IAAI,KAAgB,GAChD,EAAK,IAAI,IAAI,KAAM,EAAK,IAAI,IAAI,KAAgB,GAChD,EAAK,IAAI,IAAI,KAAM,EAAK,IAAI,IAAI,KAAgB;EAEpD,OAAO;CACX;CAOA,IAAI,OAAqB;EACrB,IAAM,IAAM,IAAI,aAAa,KAAK,SAAS,CAAC;EAC5C,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAM7B,AALA,EAAI,IAAI,KAAK,KAAK,KAAK,IAAI,IAC3B,EAAI,IAAI,IAAI,KAAK,KAAK,KAAK,IAAI,IAAI,IACnC,EAAI,IAAI,IAAI,KAAK,KAAK,KAAK,IAAI,IAAI,IACnC,EAAI,IAAI,IAAI,KAAK,KAAK,KAAK,IAAI,IAAI,IACnC,EAAI,IAAI,IAAI,KAAK,KAAK,KAAK,IAC3B,EAAI,IAAI,IAAI,KAAK,KAAK,IAAI;EAE9B,OAAO;CACX;AACJ,GAOa,IAAb,MAAmB;CAEa;CAA5B,YAAY,GAAoC;EAApB,KAAA,OAAA;CAAqB;CAGjD,IAAI,SAAiB;EACjB,OAAO,KAAK,KAAK;CACrB;CAGA,IAAI,QAA2B;EAC3B,OAAO,CAAC,KAAK,MAAM;CACvB;CAGA,IAAI,OAAe;EACf,IAAI,KAAK,KAAK,WAAW,GAAG,OAAO;EACnC,IAAI,IAAO,GACP,IAAU,KAAK,KAAK;EACxB,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK,QAAQ,KAAK;GACvC,IAAM,IAAI,KAAK,KAAK;GACpB,AAAI,IAAI,MACJ,IAAO,GACP,IAAU;EAElB;EACA,OAAO;CACX;CAGA,IAAI,WAAmB;EAEnB,OADI,KAAK,KAAK,WAAW,IAAU,IAC5B,KAAK,KAAK,KAAK;CAC1B;CAGA,IAAI,OAAmB;EACnB,OAAO,KAAK,MAAM,CAAC,CAAC,CAAC;CACzB;CAGA,IAAI,WAAyB;EACzB,OAAO,KAAK,MAAM,CAAC,CAAC,CAAC;CACzB;CAEA,MAAc,GAA0D;EACpE,IAAM,IAAI,KAAK,IAAI,GAAG,KAAK,KAAK,MAAM,GAChC,IAAkB,CAAC;EACzB,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK,QAAQ,KAAK,EAAM,KAAK,CAAC;EACvD,EAAM,MAAM,GAAG,MAAO,KAAK,KAAK,KAAiB,KAAK,KAAK,EAAa;EACxE,IAAM,IAAU,IAAI,WAAW,CAAC,GAC1B,IAAS,IAAI,aAAa,CAAC;EACjC,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAEnB,AADA,EAAQ,KAAK,EAAM,IACnB,EAAO,KAAK,KAAK,KAAK,EAAM;EAEhC,OAAO;GAAE;GAAS;EAAO;CAC7B;AACJ,GAQa,IAAb,MAAmB;CAOK;CAKA;CACA;CAPpB,YACI,GAKA,GACA,GACF;EADkB,AANA,KAAA,OAAA,GAKA,KAAA,OAAA,GACA,KAAA,YAAA;CACjB;CAGH,IAAI,SAAiB;EACjB,OAAO,KAAK,KAAK;CACrB;CAGA,IAAI,QAA2B;EAC3B,OAAO,CAAC,KAAK,MAAM;CACvB;CAEA,CAAC,OAAO,YAIL;EACC,OAAO,KAAK,KAAK,OAAO,SAAS,CAAC;CACtC;AACJ,GAiBa,IAAb,MAAmE;CAE3C;CACA;CACA;CACA;CACA;CACA;CACA;CAPpB,YACI,GACA,GACA,GACA,GACA,GACA,IAAsC,MACtC,IAAyC,GAC3C;EADkB,AANA,KAAA,QAAA,GACA,KAAA,aAAA,GACA,KAAA,QAAA,GACA,KAAA,UAAA,GACA,KAAA,YAAA,GACA,KAAA,OAAA,GACA,KAAA,QAAA;CACjB;CAGH,IAAI,SAAiB;EACjB,OAAO,KAAK,WAAW;CAC3B;CAGA,IAAI,GAA4C;EAC5C,OAAO,KAAK,WAAW;CAC3B;CAEA,CAAC,OAAO,YAAuC;EAC3C,OAAO,KAAK,WAAW,OAAO,SAAS,CAAC;CAC5C;AACJ,GAmBa,IAAb,MAAwE;CAEhD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CARpB,YACI,GACA,GACA,GACA,GACA,GACA,GACA,IAAsC,MACtC,IAAyC,GAC3C;EADkB,AAPA,KAAA,QAAA,GACA,KAAA,aAAA,GACA,KAAA,QAAA,GACA,KAAA,kBAAA,GACA,KAAA,UAAA,GACA,KAAA,YAAA,GACA,KAAA,OAAA,GACA,KAAA,QAAA;CACjB;CAGH,IAAI,SAAiB;EACjB,OAAO,KAAK,WAAW;CAC3B;CAGA,IAAI,GAA4C;EAC5C,OAAO,KAAK,WAAW;CAC3B;CAEA,CAAC,OAAO,YAAuC;EAC3C,OAAO,KAAK,WAAW,OAAO,SAAS,CAAC;CAC5C;AACJ,GAKa,IAAb,MAAmC;CAEX;CACA;CACA;CACA;CACA;CACA;CACA;CAPpB,YACI,GACA,GACA,GACA,GACA,GACA,IAAsC,MACtC,IAAyC,GAC3C;EADkB,AANA,KAAA,QAAA,GACA,KAAA,SAAA,GACA,KAAA,QAAA,GACA,KAAA,UAAA,GACA,KAAA,YAAA,GACA,KAAA,OAAA,GACA,KAAA,QAAA;CACjB;CAGH,IAAI,MAAc;EACd,OAAO,KAAK,MAAM;CACtB;CAGA,IAAI,OAAe;EACf,OAAO,KAAK,MAAM;CACtB;CAGA,IAAI,OAAe;EACf,OAAO,KAAK,MAAM,KAAK,QAAQ,SAAS,KAAK;CACjD;CAGA,IAAI,gBAA0E;EAC1E,OAAO,KAAK,OAAO;CACvB;AACJ,GAQa,IAAb,MAAyE;CAEjD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CARpB,YACI,GACA,GACA,GACA,GACA,GACA,GACA,IAAsC,MACtC,IAAyC,GAC3C;EADkB,AAPA,KAAA,QAAA,GACA,KAAA,QAAA,GACA,KAAA,aAAA,GACA,KAAA,QAAA,GACA,KAAA,UAAA,GACA,KAAA,YAAA,GACA,KAAA,OAAA,GACA,KAAA,QAAA;CACjB;CAGH,IAAI,SAAiB;EACjB,OAAO,KAAK,WAAW;CAC3B;CAGA,IAAI,GAA+C;EAC/C,OAAO,KAAK,WAAW;CAC3B;CAEA,CAAC,OAAO,YAA0C;EAC9C,OAAO,KAAK,WAAW,OAAO,SAAS,CAAC;CAC5C;AACJ"}
|
|
1
|
+
{"version":3,"file":"results.js","names":[],"sources":["../../src/vision/results.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Per-image result envelopes — Ultralytics-style `Results` for the SDK.\n *\n * Each `predict()` call returns a 1-element array of these envelopes,\n * mirroring `YOLO(\"img.jpg\")`. Each envelope holds:\n *\n * - A bulk-array view of the predictions (`Boxes`, `Probs`, `Masks`) with\n * the exact attribute names Ultralytics uses (`xyxy`, `xywh`, `xyxyn`,\n * `xywhn`, `cls`, `conf`, `data`, `top1`, `top5`).\n * - Per-instance dataclasses (`DetectionResult`, `SegmentationResult`,\n * `ClassProbability`) for callers who prefer the OO interface.\n * - `names`: `Record<number, string>` matching Ultralytics' `model.names`.\n * - `origImg` / `origShape` / `path`: provenance for the original input.\n * - `speed`: per-stage timings of the `predict()` call that produced it.\n */\n\nimport { type Speed } from \"./core/timing\";\nimport {\n type ClassificationResult,\n type DetectionResult,\n type RGBImage,\n type SegmentationResult,\n} from \"./types\";\n\n/**\n * Zero timings for an envelope built outside a `predict()` call.\n *\n * Hand-constructed envelopes (tests, adapters) have nothing to measure, and\n * zeros keep `speed.inference` a plain `number` at every call site instead of\n * forcing an optional check that only ever fires for synthetic data.\n */\nconst NO_SPEED: Speed = { load: 0, preprocess: 0, inference: 0, postprocess: 0 };\n\n/**\n * Bulk numpy-style view of detected boxes for a single image.\n *\n * Mirrors Ultralytics' `Boxes` interface. Coordinates in {@link xyxy} and\n * {@link xywh} are absolute pixels in the original image; the `*n` variants\n * are normalized to `[0, 1]` using `origShape`.\n */\nexport class Boxes {\n /**\n * @param xyxy Flat array of length `4 * N` in `[x1, y1, x2, y2, ...]` order.\n * @param cls One class index per box, length `N`.\n * @param conf One confidence per box, length `N`.\n * @param origShape `[height, width]` of the original image.\n */\n constructor(\n public readonly xyxy: Float32Array,\n public readonly cls: Int32Array,\n public readonly conf: Float32Array,\n public readonly origShape: readonly [number, number],\n ) {}\n\n /** Number of detected boxes. */\n get length(): number {\n return this.cls.length;\n }\n\n /** `[N, 4]` shape of the `xyxy` view. */\n get shape(): readonly [number, number] {\n return [this.length, 4];\n }\n\n /** Boxes as `[N, 4]` `[cx, cy, w, h]` flat array in absolute pixels. */\n get xywh(): Float32Array {\n const out = new Float32Array(this.xyxy.length);\n for (let i = 0; i < this.length; i++) {\n const x1 = this.xyxy[i * 4] as number;\n const y1 = this.xyxy[i * 4 + 1] as number;\n const x2 = this.xyxy[i * 4 + 2] as number;\n const y2 = this.xyxy[i * 4 + 3] as number;\n out[i * 4] = (x1 + x2) / 2;\n out[i * 4 + 1] = (y1 + y2) / 2;\n out[i * 4 + 2] = x2 - x1;\n out[i * 4 + 3] = y2 - y1;\n }\n return out;\n }\n\n /** Boxes as `[N, 4]` `[x1, y1, x2, y2]` normalized to `[0, 1]`. */\n get xyxyn(): Float32Array {\n const [h, w] = this.origShape;\n const out = new Float32Array(this.xyxy.length);\n if (this.length === 0 || w <= 0 || h <= 0) return out;\n for (let i = 0; i < this.length; i++) {\n out[i * 4] = (this.xyxy[i * 4] as number) / w;\n out[i * 4 + 1] = (this.xyxy[i * 4 + 1] as number) / h;\n out[i * 4 + 2] = (this.xyxy[i * 4 + 2] as number) / w;\n out[i * 4 + 3] = (this.xyxy[i * 4 + 3] as number) / h;\n }\n return out;\n }\n\n /** Boxes as `[N, 4]` `[cx, cy, w, h]` normalized to `[0, 1]`. */\n get xywhn(): Float32Array {\n const xywh = this.xywh;\n const [h, w] = this.origShape;\n if (this.length === 0 || w <= 0 || h <= 0) return xywh;\n for (let i = 0; i < this.length; i++) {\n xywh[i * 4] = (xywh[i * 4] as number) / w;\n xywh[i * 4 + 1] = (xywh[i * 4 + 1] as number) / h;\n xywh[i * 4 + 2] = (xywh[i * 4 + 2] as number) / w;\n xywh[i * 4 + 3] = (xywh[i * 4 + 3] as number) / h;\n }\n return xywh;\n }\n\n /**\n * Concatenated `[N, 6]` array of `[x1, y1, x2, y2, conf, cls]`.\n *\n * Matches Ultralytics' `boxes.data`.\n */\n get data(): Float32Array {\n const out = new Float32Array(this.length * 6);\n for (let i = 0; i < this.length; i++) {\n out[i * 6] = this.xyxy[i * 4] as number;\n out[i * 6 + 1] = this.xyxy[i * 4 + 1] as number;\n out[i * 6 + 2] = this.xyxy[i * 4 + 2] as number;\n out[i * 6 + 3] = this.xyxy[i * 4 + 3] as number;\n out[i * 6 + 4] = this.conf[i] as number;\n out[i * 6 + 5] = this.cls[i] as number;\n }\n return out;\n }\n}\n\n/** One selection of the highest-probability classes, descending. */\ninterface TopK {\n indices: Int32Array;\n values: Float32Array;\n}\n\n/**\n * Top-k classification probabilities for a single image.\n *\n * Mirrors Ultralytics' `Probs` interface.\n */\nexport class Probs {\n /** @param data `[numClasses]` per-class probabilities, indexed by class id. */\n constructor(public readonly data: Float32Array) {}\n\n /** Number of classes. */\n get length(): number {\n return this.data.length;\n }\n\n /** `[numClasses]` shape of the underlying vector. */\n get shape(): readonly [number] {\n return [this.length];\n }\n\n /** Index of the most probable class. */\n get top1(): number {\n return this._top(1).indices[0] ?? 0;\n }\n\n /** Probability of the top-1 class. */\n get top1conf(): number {\n return this._top(1).values[0] ?? 0;\n }\n\n /** Indices of the top-5 most probable classes, descending. */\n get top5(): Int32Array {\n return this._top(5).indices;\n }\n\n /** Probabilities of the top-5 classes, descending. */\n get top5conf(): Float32Array {\n return this._top(5).values;\n }\n\n private _cache = new Map<number, TopK>();\n\n /**\n * Memoised {@link Probs._topK}.\n *\n * `top5` and `top5conf` are separate getters over the same selection, so a\n * caller reading both would otherwise pay for it twice — once per frame, in\n * a camera loop. The probabilities a `Probs` was built from do not change,\n * so the result is computed once per `k` and kept.\n *\n * @param k - How many classes to select.\n * @returns The cached selection for that `k`.\n */\n private _top(k: number): TopK {\n const hit = this._cache.get(k);\n if (hit) return hit;\n const computed = this._topK(k);\n this._cache.set(k, computed);\n return computed;\n }\n\n /**\n * Select the `k` highest probabilities without ordering the rest.\n *\n * A full sort to read five entries out of a thousand-class vector costs\n * O(n log n) plus an index array the size of the vector; keeping `k` slots\n * ordered by insertion and scanning once costs O(n·k) with no allocation\n * beyond the result. Measured on 1000 classes, that is 123.7 µs against\n * 1.9 µs for the same output.\n *\n * Ties keep the lower class index first, matching the stable sort this\n * replaced: a candidate only displaces an entry it is strictly greater\n * than.\n *\n * @param k - How many classes to select.\n * @returns Indices and probabilities, descending by probability.\n */\n private _topK(k: number): TopK {\n const size = Math.min(k, this.data.length);\n const indices = new Int32Array(size);\n const values = new Float32Array(size);\n if (size === 0) return { indices, values };\n\n values.fill(Number.NEGATIVE_INFINITY);\n for (let i = 0; i < this.data.length; i++) {\n const value = this.data[i] as number;\n if (value <= (values[size - 1] as number)) continue;\n let slot = size - 1;\n while (slot > 0 && (values[slot - 1] as number) < value) {\n values[slot] = values[slot - 1] as number;\n indices[slot] = indices[slot - 1] as number;\n slot -= 1;\n }\n values[slot] = value;\n indices[slot] = i;\n }\n return { indices, values };\n }\n}\n\n/**\n * Per-instance binary masks for a single image.\n *\n * Each mask is cropped to its instance's bounding box. To paint masks onto\n * a full-image canvas, use `xyxy[i]` as the top-left target.\n */\nexport class Masks {\n /**\n * @param data Per-instance binary masks (`Mask` objects from `types.ts`).\n * @param xyxy Flat `[N, 4]` of bounding-box coordinates in original pixels.\n * @param origShape `[height, width]` of the original image.\n */\n constructor(\n public readonly data: ReadonlyArray<{\n readonly data: Uint8Array;\n readonly width: number;\n readonly height: number;\n }>,\n public readonly xyxy: Float32Array,\n public readonly origShape: readonly [number, number],\n ) {}\n\n /** Number of instance masks. */\n get length(): number {\n return this.data.length;\n }\n\n /** `[N]` shape of the masks collection. */\n get shape(): readonly [number] {\n return [this.length];\n }\n\n [Symbol.iterator](): Iterator<{\n readonly data: Uint8Array;\n readonly width: number;\n readonly height: number;\n }> {\n return this.data[Symbol.iterator]();\n }\n}\n\n/**\n * Per-image detection envelope (Ultralytics-style `Results`).\n *\n * Iterating yields per-instance {@link DetectionResult} entries, so legacy\n * code that did `for (const d of detector.predict(img))` only needs an\n * extra `[0]` to bridge:\n *\n * ```typescript\n * for (const d of (await detector.predict(img))[0]) {\n * console.log(d.cls, d.conf, d.box.xyxy);\n * }\n * ```\n *\n * For numpy-style bulk access, use the `boxes` collection.\n */\nexport class DetectionResults implements Iterable<DetectionResult> {\n constructor(\n public readonly boxes: Boxes,\n public readonly detections: readonly DetectionResult[],\n public readonly names: Readonly<Record<number, string>>,\n public readonly origImg: RGBImage,\n public readonly origShape: readonly [number, number],\n public readonly path: string | null = null,\n public readonly speed: Readonly<Speed> = NO_SPEED,\n ) {}\n\n /** Number of surviving detections. */\n get length(): number {\n return this.detections.length;\n }\n\n /** Index into the per-instance detections. */\n get(index: number): DetectionResult | undefined {\n return this.detections[index];\n }\n\n [Symbol.iterator](): Iterator<DetectionResult> {\n return this.detections[Symbol.iterator]();\n }\n}\n\n/**\n * Per-image envelope for a fused detect→classify pipeline.\n *\n * Structurally a {@link DetectionResults} with a second class map: every\n * detection it yields carries a populated `classification`, and the two stages\n * have their own, unrelated label spaces — a detector that finds `sheep`\n * feeding a classifier that answers `famacha_3` shares no class ids with it.\n * Merging them into one `names` record would make `cls` and\n * `classification.cls` look comparable when they are not.\n *\n * ```typescript\n * const result = (await pipeline.predict(\"flock.jpg\"))[0];\n * for (const detection of result) {\n * console.log(detection.name, detection.conf, detection.classification?.name);\n * }\n * ```\n */\nexport class DetectClassifyResults implements Iterable<DetectionResult> {\n constructor(\n public readonly boxes: Boxes,\n public readonly detections: readonly DetectionResult[],\n public readonly names: Readonly<Record<number, string>>,\n public readonly classifierNames: Readonly<Record<number, string>>,\n public readonly origImg: RGBImage,\n public readonly origShape: readonly [number, number],\n public readonly path: string | null = null,\n public readonly speed: Readonly<Speed> = NO_SPEED,\n ) {}\n\n /** Number of surviving detections. */\n get length(): number {\n return this.detections.length;\n }\n\n /** Index into the per-instance detections. */\n get(index: number): DetectionResult | undefined {\n return this.detections[index];\n }\n\n [Symbol.iterator](): Iterator<DetectionResult> {\n return this.detections[Symbol.iterator]();\n }\n}\n\n/**\n * Per-image classification envelope (Ultralytics-style `Results`).\n */\nexport class ClassificationResults {\n constructor(\n public readonly probs: Probs,\n public readonly result: ClassificationResult,\n public readonly names: Readonly<Record<number, string>>,\n public readonly origImg: RGBImage,\n public readonly origShape: readonly [number, number],\n public readonly path: string | null = null,\n public readonly speed: Readonly<Speed> = NO_SPEED,\n ) {}\n\n /** Top-1 class index (Ultralytics-style alias). */\n get cls(): number {\n return this.probs.top1;\n }\n\n /** Top-1 confidence (Ultralytics-style alias). */\n get conf(): number {\n return this.probs.top1conf;\n }\n\n /** Top-1 class name. */\n get name(): string {\n return this.names[this.cls] ?? `class_${this.cls}`;\n }\n\n /** Per-class probability list, sorted descending (legacy field). */\n get probabilities(): readonly ClassificationResult[\"probabilities\"][number][] {\n return this.result.probabilities;\n }\n}\n\n/**\n * Per-image instance-segmentation envelope (Ultralytics-style `Results`).\n *\n * Iterating yields per-instance {@link SegmentationResult} entries. `boxes`\n * and `masks` mirror Ultralytics' bulk-array views.\n */\nexport class SegmentationResults implements Iterable<SegmentationResult> {\n constructor(\n public readonly boxes: Boxes,\n public readonly masks: Masks,\n public readonly detections: readonly SegmentationResult[],\n public readonly names: Readonly<Record<number, string>>,\n public readonly origImg: RGBImage,\n public readonly origShape: readonly [number, number],\n public readonly path: string | null = null,\n public readonly speed: Readonly<Speed> = NO_SPEED,\n ) {}\n\n /** Number of surviving instances. */\n get length(): number {\n return this.detections.length;\n }\n\n /** Index into the per-instance results. */\n get(index: number): SegmentationResult | undefined {\n return this.detections[index];\n }\n\n [Symbol.iterator](): Iterator<SegmentationResult> {\n return this.detections[Symbol.iterator]();\n }\n}\n"],"mappings":"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"}
|
package/dist/vision.d.ts
CHANGED
|
@@ -1533,6 +1533,35 @@ export declare const DEFAULT_PROVIDERS: readonly string[];
|
|
|
1533
1533
|
get top5(): Int32Array;
|
|
1534
1534
|
/** Probabilities of the top-5 classes, descending. */
|
|
1535
1535
|
get top5conf(): Float32Array;
|
|
1536
|
+
private _cache;
|
|
1537
|
+
/**
|
|
1538
|
+
* Memoised {@link Probs._topK}.
|
|
1539
|
+
*
|
|
1540
|
+
* `top5` and `top5conf` are separate getters over the same selection, so a
|
|
1541
|
+
* caller reading both would otherwise pay for it twice — once per frame, in
|
|
1542
|
+
* a camera loop. The probabilities a `Probs` was built from do not change,
|
|
1543
|
+
* so the result is computed once per `k` and kept.
|
|
1544
|
+
*
|
|
1545
|
+
* @param k - How many classes to select.
|
|
1546
|
+
* @returns The cached selection for that `k`.
|
|
1547
|
+
*/
|
|
1548
|
+
private _top;
|
|
1549
|
+
/**
|
|
1550
|
+
* Select the `k` highest probabilities without ordering the rest.
|
|
1551
|
+
*
|
|
1552
|
+
* A full sort to read five entries out of a thousand-class vector costs
|
|
1553
|
+
* O(n log n) plus an index array the size of the vector; keeping `k` slots
|
|
1554
|
+
* ordered by insertion and scanning once costs O(n·k) with no allocation
|
|
1555
|
+
* beyond the result. Measured on 1000 classes, that is 123.7 µs against
|
|
1556
|
+
* 1.9 µs for the same output.
|
|
1557
|
+
*
|
|
1558
|
+
* Ties keep the lower class index first, matching the stable sort this
|
|
1559
|
+
* replaced: a candidate only displaces an entry it is strictly greater
|
|
1560
|
+
* than.
|
|
1561
|
+
*
|
|
1562
|
+
* @param k - How many classes to select.
|
|
1563
|
+
* @returns Indices and probabilities, descending by probability.
|
|
1564
|
+
*/
|
|
1536
1565
|
private _topK;
|
|
1537
1566
|
}
|
|
1538
1567
|
|
|
@@ -5,5 +5,5 @@ export const staticIcons = {
|
|
|
5
5
|
${n.map(e=>` "${e}": ${m(r.get(e))},`).join(`
|
|
6
6
|
`)}
|
|
7
7
|
};
|
|
8
|
-
`}function f(e){return e.split(`-`).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(``)}var p=new Set([`Infinity`,`NaN`,`undefined`,`eval`,`arguments`]);function m(e){return p.has(e)?`${e}Icon`:e}async function h(e,t){let i=[],a;try{a=await(0,r.readdir)(e,{withFileTypes:!0})}catch{return i}for(let r of a)if(r.isDirectory()){if(t.has(r.name)||r.name.startsWith(`.`))continue;i.push(...await h((0,n.join)(e,r.name),t))}else r.isFile()&&c.test(r.name)&&i.push((0,n.join)(e,r.name));return i}function g(i={}){let{dir:a=`src`,include:s=[],skipDirs:f}=i,p=new Set(t.iconNames),m=new Set(f??l),g=new Set(s.filter(e=>p.has(e))),_=process.cwd(),v=async()=>{let e=await h((0,n.resolve)(_,a),m);for(let t of e)try{let e=await(0,r.readFile)(t,`utf8`);for(let t of u(e,p))g.add(t)}catch{}};return{name:`tempest-icons`,enforce:`pre`,configResolved(e){_=e.root??_},async buildStart(){await v()},resolveId(e){return e===`tempest-react-sdk/icons/virtual`||e===`virtual:tempest-icons`?o:null},load(t){return t===o?d([...g],e.iconAliases):null},async handleHotUpdate(e){if(!c.test(e.file))return;let t=g.size;for(let t of u(await e.read(),p))g.add(t);if(g.size===t)return;let n=e.server.moduleGraph.getModuleById(o);
|
|
8
|
+
`}function f(e){return e.split(`-`).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(``)}var p=new Set([`Infinity`,`NaN`,`undefined`,`eval`,`arguments`]);function m(e){return p.has(e)?`${e}Icon`:e}async function h(e,t){let i=[],a;try{a=await(0,r.readdir)(e,{withFileTypes:!0})}catch{return i}for(let r of a)if(r.isDirectory()){if(t.has(r.name)||r.name.startsWith(`.`))continue;i.push(...await h((0,n.join)(e,r.name),t))}else r.isFile()&&c.test(r.name)&&i.push((0,n.join)(e,r.name));return i}function g(i={}){let{dir:a=`src`,include:s=[],skipDirs:f}=i,p=new Set(t.iconNames),m=new Set(f??l),g=new Set(s.filter(e=>p.has(e))),_=process.cwd(),v=async()=>{let e=await h((0,n.resolve)(_,a),m);for(let t of e)try{let e=await(0,r.readFile)(t,`utf8`);for(let t of u(e,p))g.add(t)}catch{}};return{name:`tempest-icons`,enforce:`pre`,configResolved(e){_=e.root??_},async buildStart(){await v()},resolveId(e){return e===`tempest-react-sdk/icons/virtual`||e===`virtual:tempest-icons`?o:null},load(t){return t===o?d([...g],e.iconAliases):null},async handleHotUpdate(e){if(!c.test(e.file))return;let t=g.size;for(let t of u(await e.read(),p))g.add(t);if(g.size===t)return;let n=e.server.moduleGraph.getModuleById(o);return n?[n]:void 0}}}exports.TEMPEST_ICONS_ID=i,exports.TEMPEST_ICONS_VIRTUAL_ID=a,exports.buildIconsModule=d,exports.scanIconSlugs=u,exports.tempestIcons=g;
|
|
9
9
|
//# sourceMappingURL=tempest-icons.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tempest-icons.cjs","names":[],"sources":["../../src/vite/tempest-icons.ts"],"sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\nimport { iconAliases } from \"@/icons/generated/aliases\";\nimport { iconNames } from \"@/icons/generated/icon-names\";\n\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** The module id an app imports to get its static registry. */\nexport const TEMPEST_ICONS_ID = \"tempest-react-sdk/icons/virtual\";\n\n/**\n * The original virtual id, still honoured.\n *\n * Published as the documented import before the subpath existed, so apps have it\n * written in their entrypoints. It resolves to the same generated module — but it\n * only ever resolves *with the plugin*, which is the reason the subpath above\n * replaced it: that one also resolves without it, to an empty stub.\n */\nexport const TEMPEST_ICONS_VIRTUAL_ID = \"virtual:tempest-icons\";\n\nconst RESOLVED_ID = `\\0${TEMPEST_ICONS_VIRTUAL_ID}`;\n\n/**\n * Slugs written as a literal `name` prop or a literal `name:` field.\n *\n * Matching the *attribute* rather than any kebab-case string is what keeps the\n * scan from sweeping in unrelated text — a CSS class, a translation key and a\n * route segment all look like slugs otherwise. Everything matched is still\n * validated against lucide's real slug list before it reaches the module.\n */\nconst NAME_PATTERNS = [\n /\\bname\\s*=\\s*[\"']([a-z0-9-]+)[\"']/g,\n /\\bname\\s*=\\s*\\{\\s*[\"']([a-z0-9-]+)[\"']\\s*\\}/g,\n /\\bname\\s*:\\s*[\"']([a-z0-9-]+)[\"']/g,\n];\n\nconst SOURCE_FILE = /\\.[cm]?[jt]sx?$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \"coverage\"]);\n\nexport interface TempestIconsOptions {\n /**\n * Directory to scan, relative to the Vite root. Default: `\"src\"`.\n */\n dir?: string;\n /**\n * Extra slugs to include even when the scan cannot see them — a name built by\n * concatenation, or one that only ever arrives from the API but is worth\n * paying for statically because it is on the first screen.\n */\n include?: readonly string[];\n /** Directory names to skip. Default: `node_modules`, `dist`, `build`, `coverage`. */\n skipDirs?: readonly string[];\n}\n\n/**\n * Collect the icon slugs a source file references literally.\n *\n * @param code - File contents.\n * @param known - The set of real lucide slugs, aliases included.\n * @returns The slugs found, validated against `known`.\n */\nexport function scanIconSlugs(code: string, known: ReadonlySet<string>): string[] {\n const found = new Set<string>();\n for (const pattern of NAME_PATTERNS) {\n pattern.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = pattern.exec(code)) !== null) {\n if (known.has(match[1])) found.add(match[1]);\n }\n }\n return [...found];\n}\n\n/**\n * Build the virtual module source for a set of slugs.\n *\n * Aliases resolve to their canonical slug before importing, because only the\n * canonical name is a real lucide export — but the alias key still points at the\n * component, so `<Icon name=\"alert-circle\" />` stays a zero-request lookup.\n *\n * @param slugs - Validated slugs to include.\n * @param aliases - Alias slug → canonical slug.\n * @returns ES module source.\n */\nexport function buildIconsModule(\n slugs: readonly string[],\n aliases: Record<string, string>,\n): string {\n const sorted = [...new Set(slugs)].sort();\n if (!sorted.length) return \"export const staticIcons = {};\\n\";\n\n const componentOf = new Map<string, string>();\n for (const slug of sorted) componentOf.set(slug, pascalCase(aliases[slug] ?? slug));\n\n const named = [...new Set(componentOf.values())]\n .sort()\n .map((name) => (localName(name) === name ? name : `${name} as ${localName(name)}`))\n .join(\", \");\n const entries = sorted\n .map((slug) => ` \"${slug}\": ${localName(componentOf.get(slug) as string)},`)\n .join(\"\\n\");\n return `import { ${named} } from \"lucide-react\";\n\nexport const staticIcons = {\n${entries}\n};\n`;\n}\n\n/** `circle-alert` → `CircleAlert`; `a-arrow-down` → `AArrowDown`. */\nfunction pascalCase(slug: string): string {\n return slug\n .split(\"-\")\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(\"\");\n}\n\n/**\n * Export names that would shadow a restricted global if bound directly.\n *\n * Lucide ships an icon literally called `infinity`, whose export is `Infinity`.\n * Binding that at module scope shadows the global, so the import is aliased.\n */\nconst RESERVED_NAMES = new Set([\"Infinity\", \"NaN\", \"undefined\", \"eval\", \"arguments\"]);\n\n/** The local binding to use for a lucide export name. */\nfunction localName(name: string): string {\n return RESERVED_NAMES.has(name) ? `${name}Icon` : name;\n}\n\n/**\n * Walk a directory and collect every source file path.\n *\n * @param dir - Absolute directory to walk.\n * @param skip - Directory names to skip.\n * @returns Absolute file paths.\n */\nasync function collectSourceFiles(dir: string, skip: ReadonlySet<string>): Promise<string[]> {\n const out: string[] = [];\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return out;\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (skip.has(entry.name) || entry.name.startsWith(\".\")) continue;\n out.push(...(await collectSourceFiles(join(dir, entry.name), skip)));\n } else if (entry.isFile() && SOURCE_FILE.test(entry.name)) {\n out.push(join(dir, entry.name));\n }\n }\n return out;\n}\n\n/**\n * Generate a static icon registry from the slugs your source mentions.\n *\n * Removes the reason to reach for `lucide-react`'s `DynamicIcon`: the slugs an app\n * writes as literals become ordinary static imports, so the bundler keeps exactly\n * those icons and the browser makes **no** extra request for them. A slug that only\n * exists at runtime still works — `<Icon>` falls back to fetching one shard per\n * initial letter.\n *\n * The scan reads the source tree directly at `buildStart` rather than harvesting\n * modules as they pass through `transform`: relying on transform order would let\n * the virtual module load before every consumer had been seen, and the registry\n * would come out short on a cold dev start — silently, since the missing slugs\n * would still render via the lazy path.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestIcons } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestIcons()] });\n *\n * @example\n * // src/main.tsx\n * import { IconProvider } from \"tempest-react-sdk/icons\";\n * import { staticIcons } from \"tempest-react-sdk/icons/virtual\";\n *\n * <IconProvider registry={staticIcons}>\n * <App />\n * </IconProvider>\n */\nexport function tempestIcons(options: TempestIconsOptions = {}): TempestVitePlugin {\n const { dir = \"src\", include = [], skipDirs } = options;\n const known = new Set<string>(iconNames);\n const skip = new Set<string>(skipDirs ?? SKIP_DIRS);\n const slugs = new Set<string>(include.filter((slug) => known.has(slug)));\n let root = process.cwd();\n\n /**\n * Rescan the source tree, keeping the explicitly included slugs.\n *\n * @tempest-limits empty-catch — the scan races the editor and the file system:\n * a file listed a moment ago can be renamed, deleted, or held by another process\n * by the time it is read. An unreadable file contributes no slugs and the build\n * continues; failing the whole scan would break `vite dev` over a temp file that\n * no longer exists.\n */\n const rescan = async (): Promise<void> => {\n const files = await collectSourceFiles(resolve(root, dir), skip);\n for (const file of files) {\n try {\n const code = await readFile(file, \"utf8\");\n for (const slug of scanIconSlugs(code, known)) slugs.add(slug);\n } catch {\n /* empty */\n }\n }\n };\n\n return {\n name: \"tempest-icons\",\n enforce: \"pre\",\n\n configResolved(config: { root?: string }) {\n root = config.root ?? root;\n },\n\n async buildStart() {\n await rescan();\n },\n\n /**\n * Claim both spellings of the registry module.\n *\n * The subpath is a real file in the installed package, so returning the\n * virtual id here is what overrides the empty stub with the generated\n * table. Letting Vite resolve it normally would silently ship the stub.\n */\n resolveId(id: string) {\n return id === TEMPEST_ICONS_ID || id === TEMPEST_ICONS_VIRTUAL_ID ? RESOLVED_ID : null;\n },\n\n load(id: string) {\n return id === RESOLVED_ID ? buildIconsModule([...slugs], iconAliases) : null;\n },\n\n /**\n * Pick up a slug added while the dev server is running.\n *\n * A newly written `<Icon name=\"save\" />` would otherwise only render via\n * the lazy path until the next restart. Rescanning the changed file and\n * invalidating the virtual module keeps dev and build agreeing on what the\n * static registry contains.\n */\n async handleHotUpdate(ctx: {\n file: string;\n read: () => Promise<string>;\n server: { moduleGraph: { getModuleById: (id: string) => unknown } };\n }) {\n if (!SOURCE_FILE.test(ctx.file)) return;\n const before = slugs.size;\n for (const slug of scanIconSlugs(await ctx.read(), known)) slugs.add(slug);\n if (slugs.size === before) return;\n const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ID);\n if (mod) return [mod];\n },\n } as TempestVitePlugin;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tempest-icons.cjs","names":[],"sources":["../../src/vite/tempest-icons.ts"],"sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\nimport { iconAliases } from \"@/icons/generated/aliases\";\nimport { iconNames } from \"@/icons/generated/icon-names\";\n\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** The module id an app imports to get its static registry. */\nexport const TEMPEST_ICONS_ID = \"tempest-react-sdk/icons/virtual\";\n\n/**\n * The original virtual id, still honoured.\n *\n * Published as the documented import before the subpath existed, so apps have it\n * written in their entrypoints. It resolves to the same generated module — but it\n * only ever resolves *with the plugin*, which is the reason the subpath above\n * replaced it: that one also resolves without it, to an empty stub.\n */\nexport const TEMPEST_ICONS_VIRTUAL_ID = \"virtual:tempest-icons\";\n\nconst RESOLVED_ID = `\\0${TEMPEST_ICONS_VIRTUAL_ID}`;\n\n/**\n * Slugs written as a literal `name` prop or a literal `name:` field.\n *\n * Matching the *attribute* rather than any kebab-case string is what keeps the\n * scan from sweeping in unrelated text — a CSS class, a translation key and a\n * route segment all look like slugs otherwise. Everything matched is still\n * validated against lucide's real slug list before it reaches the module.\n */\nconst NAME_PATTERNS = [\n /\\bname\\s*=\\s*[\"']([a-z0-9-]+)[\"']/g,\n /\\bname\\s*=\\s*\\{\\s*[\"']([a-z0-9-]+)[\"']\\s*\\}/g,\n /\\bname\\s*:\\s*[\"']([a-z0-9-]+)[\"']/g,\n];\n\nconst SOURCE_FILE = /\\.[cm]?[jt]sx?$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \"coverage\"]);\n\nexport interface TempestIconsOptions {\n /**\n * Directory to scan, relative to the Vite root. Default: `\"src\"`.\n */\n dir?: string;\n /**\n * Extra slugs to include even when the scan cannot see them — a name built by\n * concatenation, or one that only ever arrives from the API but is worth\n * paying for statically because it is on the first screen.\n */\n include?: readonly string[];\n /** Directory names to skip. Default: `node_modules`, `dist`, `build`, `coverage`. */\n skipDirs?: readonly string[];\n}\n\n/**\n * Collect the icon slugs a source file references literally.\n *\n * @param code - File contents.\n * @param known - The set of real lucide slugs, aliases included.\n * @returns The slugs found, validated against `known`.\n */\nexport function scanIconSlugs(code: string, known: ReadonlySet<string>): string[] {\n const found = new Set<string>();\n for (const pattern of NAME_PATTERNS) {\n pattern.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = pattern.exec(code)) !== null) {\n if (known.has(match[1])) found.add(match[1]);\n }\n }\n return [...found];\n}\n\n/**\n * Build the virtual module source for a set of slugs.\n *\n * Aliases resolve to their canonical slug before importing, because only the\n * canonical name is a real lucide export — but the alias key still points at the\n * component, so `<Icon name=\"alert-circle\" />` stays a zero-request lookup.\n *\n * @param slugs - Validated slugs to include.\n * @param aliases - Alias slug → canonical slug.\n * @returns ES module source.\n */\nexport function buildIconsModule(\n slugs: readonly string[],\n aliases: Record<string, string>,\n): string {\n const sorted = [...new Set(slugs)].sort();\n if (!sorted.length) return \"export const staticIcons = {};\\n\";\n\n const componentOf = new Map<string, string>();\n for (const slug of sorted) componentOf.set(slug, pascalCase(aliases[slug] ?? slug));\n\n const named = [...new Set(componentOf.values())]\n .sort()\n .map((name) => (localName(name) === name ? name : `${name} as ${localName(name)}`))\n .join(\", \");\n const entries = sorted\n .map((slug) => ` \"${slug}\": ${localName(componentOf.get(slug) as string)},`)\n .join(\"\\n\");\n return `import { ${named} } from \"lucide-react\";\n\nexport const staticIcons = {\n${entries}\n};\n`;\n}\n\n/** `circle-alert` → `CircleAlert`; `a-arrow-down` → `AArrowDown`. */\nfunction pascalCase(slug: string): string {\n return slug\n .split(\"-\")\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(\"\");\n}\n\n/**\n * Export names that would shadow a restricted global if bound directly.\n *\n * Lucide ships an icon literally called `infinity`, whose export is `Infinity`.\n * Binding that at module scope shadows the global, so the import is aliased.\n */\nconst RESERVED_NAMES = new Set([\"Infinity\", \"NaN\", \"undefined\", \"eval\", \"arguments\"]);\n\n/** The local binding to use for a lucide export name. */\nfunction localName(name: string): string {\n return RESERVED_NAMES.has(name) ? `${name}Icon` : name;\n}\n\n/**\n * Walk a directory and collect every source file path.\n *\n * @param dir - Absolute directory to walk.\n * @param skip - Directory names to skip.\n * @returns Absolute file paths.\n */\nasync function collectSourceFiles(dir: string, skip: ReadonlySet<string>): Promise<string[]> {\n const out: string[] = [];\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return out;\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (skip.has(entry.name) || entry.name.startsWith(\".\")) continue;\n out.push(...(await collectSourceFiles(join(dir, entry.name), skip)));\n } else if (entry.isFile() && SOURCE_FILE.test(entry.name)) {\n out.push(join(dir, entry.name));\n }\n }\n return out;\n}\n\n/**\n * Generate a static icon registry from the slugs your source mentions.\n *\n * Removes the reason to reach for `lucide-react`'s `DynamicIcon`: the slugs an app\n * writes as literals become ordinary static imports, so the bundler keeps exactly\n * those icons and the browser makes **no** extra request for them. A slug that only\n * exists at runtime still works — `<Icon>` falls back to fetching one shard per\n * initial letter.\n *\n * The scan reads the source tree directly at `buildStart` rather than harvesting\n * modules as they pass through `transform`: relying on transform order would let\n * the virtual module load before every consumer had been seen, and the registry\n * would come out short on a cold dev start — silently, since the missing slugs\n * would still render via the lazy path.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestIcons } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestIcons()] });\n *\n * @example\n * // src/main.tsx\n * import { IconProvider } from \"tempest-react-sdk/icons\";\n * import { staticIcons } from \"tempest-react-sdk/icons/virtual\";\n *\n * <IconProvider registry={staticIcons}>\n * <App />\n * </IconProvider>\n */\nexport function tempestIcons(options: TempestIconsOptions = {}): TempestVitePlugin {\n const { dir = \"src\", include = [], skipDirs } = options;\n const known = new Set<string>(iconNames);\n const skip = new Set<string>(skipDirs ?? SKIP_DIRS);\n const slugs = new Set<string>(include.filter((slug) => known.has(slug)));\n let root = process.cwd();\n\n /**\n * Rescan the source tree, keeping the explicitly included slugs.\n *\n * @tempest-limits empty-catch — the scan races the editor and the file system:\n * a file listed a moment ago can be renamed, deleted, or held by another process\n * by the time it is read. An unreadable file contributes no slugs and the build\n * continues; failing the whole scan would break `vite dev` over a temp file that\n * no longer exists.\n */\n const rescan = async (): Promise<void> => {\n const files = await collectSourceFiles(resolve(root, dir), skip);\n for (const file of files) {\n try {\n const code = await readFile(file, \"utf8\");\n for (const slug of scanIconSlugs(code, known)) slugs.add(slug);\n } catch {\n /* empty */\n }\n }\n };\n\n return {\n name: \"tempest-icons\",\n enforce: \"pre\",\n\n configResolved(config: { root?: string }) {\n root = config.root ?? root;\n },\n\n async buildStart() {\n await rescan();\n },\n\n /**\n * Claim both spellings of the registry module.\n *\n * The subpath is a real file in the installed package, so returning the\n * virtual id here is what overrides the empty stub with the generated\n * table. Letting Vite resolve it normally would silently ship the stub.\n */\n resolveId(id: string) {\n return id === TEMPEST_ICONS_ID || id === TEMPEST_ICONS_VIRTUAL_ID ? RESOLVED_ID : null;\n },\n\n load(id: string) {\n return id === RESOLVED_ID ? buildIconsModule([...slugs], iconAliases) : null;\n },\n\n /**\n * Pick up a slug added while the dev server is running.\n *\n * A newly written `<Icon name=\"save\" />` would otherwise only render via\n * the lazy path until the next restart. Rescanning the changed file and\n * invalidating the virtual module keeps dev and build agreeing on what the\n * static registry contains.\n */\n async handleHotUpdate(ctx: {\n file: string;\n read: () => Promise<string>;\n server: { moduleGraph: { getModuleById: (id: string) => unknown } };\n }): Promise<unknown[] | undefined> {\n if (!SOURCE_FILE.test(ctx.file)) return undefined;\n const before = slugs.size;\n for (const slug of scanIconSlugs(await ctx.read(), known)) slugs.add(slug);\n if (slugs.size === before) return undefined;\n const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ID);\n return mod ? [mod] : undefined;\n },\n } as TempestVitePlugin;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tempest-icons.js","names":[],"sources":["../../src/vite/tempest-icons.ts"],"sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\nimport { iconAliases } from \"@/icons/generated/aliases\";\nimport { iconNames } from \"@/icons/generated/icon-names\";\n\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** The module id an app imports to get its static registry. */\nexport const TEMPEST_ICONS_ID = \"tempest-react-sdk/icons/virtual\";\n\n/**\n * The original virtual id, still honoured.\n *\n * Published as the documented import before the subpath existed, so apps have it\n * written in their entrypoints. It resolves to the same generated module — but it\n * only ever resolves *with the plugin*, which is the reason the subpath above\n * replaced it: that one also resolves without it, to an empty stub.\n */\nexport const TEMPEST_ICONS_VIRTUAL_ID = \"virtual:tempest-icons\";\n\nconst RESOLVED_ID = `\\0${TEMPEST_ICONS_VIRTUAL_ID}`;\n\n/**\n * Slugs written as a literal `name` prop or a literal `name:` field.\n *\n * Matching the *attribute* rather than any kebab-case string is what keeps the\n * scan from sweeping in unrelated text — a CSS class, a translation key and a\n * route segment all look like slugs otherwise. Everything matched is still\n * validated against lucide's real slug list before it reaches the module.\n */\nconst NAME_PATTERNS = [\n /\\bname\\s*=\\s*[\"']([a-z0-9-]+)[\"']/g,\n /\\bname\\s*=\\s*\\{\\s*[\"']([a-z0-9-]+)[\"']\\s*\\}/g,\n /\\bname\\s*:\\s*[\"']([a-z0-9-]+)[\"']/g,\n];\n\nconst SOURCE_FILE = /\\.[cm]?[jt]sx?$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \"coverage\"]);\n\nexport interface TempestIconsOptions {\n /**\n * Directory to scan, relative to the Vite root. Default: `\"src\"`.\n */\n dir?: string;\n /**\n * Extra slugs to include even when the scan cannot see them — a name built by\n * concatenation, or one that only ever arrives from the API but is worth\n * paying for statically because it is on the first screen.\n */\n include?: readonly string[];\n /** Directory names to skip. Default: `node_modules`, `dist`, `build`, `coverage`. */\n skipDirs?: readonly string[];\n}\n\n/**\n * Collect the icon slugs a source file references literally.\n *\n * @param code - File contents.\n * @param known - The set of real lucide slugs, aliases included.\n * @returns The slugs found, validated against `known`.\n */\nexport function scanIconSlugs(code: string, known: ReadonlySet<string>): string[] {\n const found = new Set<string>();\n for (const pattern of NAME_PATTERNS) {\n pattern.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = pattern.exec(code)) !== null) {\n if (known.has(match[1])) found.add(match[1]);\n }\n }\n return [...found];\n}\n\n/**\n * Build the virtual module source for a set of slugs.\n *\n * Aliases resolve to their canonical slug before importing, because only the\n * canonical name is a real lucide export — but the alias key still points at the\n * component, so `<Icon name=\"alert-circle\" />` stays a zero-request lookup.\n *\n * @param slugs - Validated slugs to include.\n * @param aliases - Alias slug → canonical slug.\n * @returns ES module source.\n */\nexport function buildIconsModule(\n slugs: readonly string[],\n aliases: Record<string, string>,\n): string {\n const sorted = [...new Set(slugs)].sort();\n if (!sorted.length) return \"export const staticIcons = {};\\n\";\n\n const componentOf = new Map<string, string>();\n for (const slug of sorted) componentOf.set(slug, pascalCase(aliases[slug] ?? slug));\n\n const named = [...new Set(componentOf.values())]\n .sort()\n .map((name) => (localName(name) === name ? name : `${name} as ${localName(name)}`))\n .join(\", \");\n const entries = sorted\n .map((slug) => ` \"${slug}\": ${localName(componentOf.get(slug) as string)},`)\n .join(\"\\n\");\n return `import { ${named} } from \"lucide-react\";\n\nexport const staticIcons = {\n${entries}\n};\n`;\n}\n\n/** `circle-alert` → `CircleAlert`; `a-arrow-down` → `AArrowDown`. */\nfunction pascalCase(slug: string): string {\n return slug\n .split(\"-\")\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(\"\");\n}\n\n/**\n * Export names that would shadow a restricted global if bound directly.\n *\n * Lucide ships an icon literally called `infinity`, whose export is `Infinity`.\n * Binding that at module scope shadows the global, so the import is aliased.\n */\nconst RESERVED_NAMES = new Set([\"Infinity\", \"NaN\", \"undefined\", \"eval\", \"arguments\"]);\n\n/** The local binding to use for a lucide export name. */\nfunction localName(name: string): string {\n return RESERVED_NAMES.has(name) ? `${name}Icon` : name;\n}\n\n/**\n * Walk a directory and collect every source file path.\n *\n * @param dir - Absolute directory to walk.\n * @param skip - Directory names to skip.\n * @returns Absolute file paths.\n */\nasync function collectSourceFiles(dir: string, skip: ReadonlySet<string>): Promise<string[]> {\n const out: string[] = [];\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return out;\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (skip.has(entry.name) || entry.name.startsWith(\".\")) continue;\n out.push(...(await collectSourceFiles(join(dir, entry.name), skip)));\n } else if (entry.isFile() && SOURCE_FILE.test(entry.name)) {\n out.push(join(dir, entry.name));\n }\n }\n return out;\n}\n\n/**\n * Generate a static icon registry from the slugs your source mentions.\n *\n * Removes the reason to reach for `lucide-react`'s `DynamicIcon`: the slugs an app\n * writes as literals become ordinary static imports, so the bundler keeps exactly\n * those icons and the browser makes **no** extra request for them. A slug that only\n * exists at runtime still works — `<Icon>` falls back to fetching one shard per\n * initial letter.\n *\n * The scan reads the source tree directly at `buildStart` rather than harvesting\n * modules as they pass through `transform`: relying on transform order would let\n * the virtual module load before every consumer had been seen, and the registry\n * would come out short on a cold dev start — silently, since the missing slugs\n * would still render via the lazy path.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestIcons } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestIcons()] });\n *\n * @example\n * // src/main.tsx\n * import { IconProvider } from \"tempest-react-sdk/icons\";\n * import { staticIcons } from \"tempest-react-sdk/icons/virtual\";\n *\n * <IconProvider registry={staticIcons}>\n * <App />\n * </IconProvider>\n */\nexport function tempestIcons(options: TempestIconsOptions = {}): TempestVitePlugin {\n const { dir = \"src\", include = [], skipDirs } = options;\n const known = new Set<string>(iconNames);\n const skip = new Set<string>(skipDirs ?? SKIP_DIRS);\n const slugs = new Set<string>(include.filter((slug) => known.has(slug)));\n let root = process.cwd();\n\n /**\n * Rescan the source tree, keeping the explicitly included slugs.\n *\n * @tempest-limits empty-catch — the scan races the editor and the file system:\n * a file listed a moment ago can be renamed, deleted, or held by another process\n * by the time it is read. An unreadable file contributes no slugs and the build\n * continues; failing the whole scan would break `vite dev` over a temp file that\n * no longer exists.\n */\n const rescan = async (): Promise<void> => {\n const files = await collectSourceFiles(resolve(root, dir), skip);\n for (const file of files) {\n try {\n const code = await readFile(file, \"utf8\");\n for (const slug of scanIconSlugs(code, known)) slugs.add(slug);\n } catch {\n /* empty */\n }\n }\n };\n\n return {\n name: \"tempest-icons\",\n enforce: \"pre\",\n\n configResolved(config: { root?: string }) {\n root = config.root ?? root;\n },\n\n async buildStart() {\n await rescan();\n },\n\n /**\n * Claim both spellings of the registry module.\n *\n * The subpath is a real file in the installed package, so returning the\n * virtual id here is what overrides the empty stub with the generated\n * table. Letting Vite resolve it normally would silently ship the stub.\n */\n resolveId(id: string) {\n return id === TEMPEST_ICONS_ID || id === TEMPEST_ICONS_VIRTUAL_ID ? RESOLVED_ID : null;\n },\n\n load(id: string) {\n return id === RESOLVED_ID ? buildIconsModule([...slugs], iconAliases) : null;\n },\n\n /**\n * Pick up a slug added while the dev server is running.\n *\n * A newly written `<Icon name=\"save\" />` would otherwise only render via\n * the lazy path until the next restart. Rescanning the changed file and\n * invalidating the virtual module keeps dev and build agreeing on what the\n * static registry contains.\n */\n async handleHotUpdate(ctx: {\n file: string;\n read: () => Promise<string>;\n server: { moduleGraph: { getModuleById: (id: string) => unknown } };\n }) {\n if (!SOURCE_FILE.test(ctx.file)) return;\n const before = slugs.size;\n for (const slug of scanIconSlugs(await ctx.read(), known)) slugs.add(slug);\n if (slugs.size === before) return;\n const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ID);\n if (mod) return [mod];\n },\n } as TempestVitePlugin;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tempest-icons.js","names":[],"sources":["../../src/vite/tempest-icons.ts"],"sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\nimport { iconAliases } from \"@/icons/generated/aliases\";\nimport { iconNames } from \"@/icons/generated/icon-names\";\n\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** The module id an app imports to get its static registry. */\nexport const TEMPEST_ICONS_ID = \"tempest-react-sdk/icons/virtual\";\n\n/**\n * The original virtual id, still honoured.\n *\n * Published as the documented import before the subpath existed, so apps have it\n * written in their entrypoints. It resolves to the same generated module — but it\n * only ever resolves *with the plugin*, which is the reason the subpath above\n * replaced it: that one also resolves without it, to an empty stub.\n */\nexport const TEMPEST_ICONS_VIRTUAL_ID = \"virtual:tempest-icons\";\n\nconst RESOLVED_ID = `\\0${TEMPEST_ICONS_VIRTUAL_ID}`;\n\n/**\n * Slugs written as a literal `name` prop or a literal `name:` field.\n *\n * Matching the *attribute* rather than any kebab-case string is what keeps the\n * scan from sweeping in unrelated text — a CSS class, a translation key and a\n * route segment all look like slugs otherwise. Everything matched is still\n * validated against lucide's real slug list before it reaches the module.\n */\nconst NAME_PATTERNS = [\n /\\bname\\s*=\\s*[\"']([a-z0-9-]+)[\"']/g,\n /\\bname\\s*=\\s*\\{\\s*[\"']([a-z0-9-]+)[\"']\\s*\\}/g,\n /\\bname\\s*:\\s*[\"']([a-z0-9-]+)[\"']/g,\n];\n\nconst SOURCE_FILE = /\\.[cm]?[jt]sx?$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \"coverage\"]);\n\nexport interface TempestIconsOptions {\n /**\n * Directory to scan, relative to the Vite root. Default: `\"src\"`.\n */\n dir?: string;\n /**\n * Extra slugs to include even when the scan cannot see them — a name built by\n * concatenation, or one that only ever arrives from the API but is worth\n * paying for statically because it is on the first screen.\n */\n include?: readonly string[];\n /** Directory names to skip. Default: `node_modules`, `dist`, `build`, `coverage`. */\n skipDirs?: readonly string[];\n}\n\n/**\n * Collect the icon slugs a source file references literally.\n *\n * @param code - File contents.\n * @param known - The set of real lucide slugs, aliases included.\n * @returns The slugs found, validated against `known`.\n */\nexport function scanIconSlugs(code: string, known: ReadonlySet<string>): string[] {\n const found = new Set<string>();\n for (const pattern of NAME_PATTERNS) {\n pattern.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = pattern.exec(code)) !== null) {\n if (known.has(match[1])) found.add(match[1]);\n }\n }\n return [...found];\n}\n\n/**\n * Build the virtual module source for a set of slugs.\n *\n * Aliases resolve to their canonical slug before importing, because only the\n * canonical name is a real lucide export — but the alias key still points at the\n * component, so `<Icon name=\"alert-circle\" />` stays a zero-request lookup.\n *\n * @param slugs - Validated slugs to include.\n * @param aliases - Alias slug → canonical slug.\n * @returns ES module source.\n */\nexport function buildIconsModule(\n slugs: readonly string[],\n aliases: Record<string, string>,\n): string {\n const sorted = [...new Set(slugs)].sort();\n if (!sorted.length) return \"export const staticIcons = {};\\n\";\n\n const componentOf = new Map<string, string>();\n for (const slug of sorted) componentOf.set(slug, pascalCase(aliases[slug] ?? slug));\n\n const named = [...new Set(componentOf.values())]\n .sort()\n .map((name) => (localName(name) === name ? name : `${name} as ${localName(name)}`))\n .join(\", \");\n const entries = sorted\n .map((slug) => ` \"${slug}\": ${localName(componentOf.get(slug) as string)},`)\n .join(\"\\n\");\n return `import { ${named} } from \"lucide-react\";\n\nexport const staticIcons = {\n${entries}\n};\n`;\n}\n\n/** `circle-alert` → `CircleAlert`; `a-arrow-down` → `AArrowDown`. */\nfunction pascalCase(slug: string): string {\n return slug\n .split(\"-\")\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(\"\");\n}\n\n/**\n * Export names that would shadow a restricted global if bound directly.\n *\n * Lucide ships an icon literally called `infinity`, whose export is `Infinity`.\n * Binding that at module scope shadows the global, so the import is aliased.\n */\nconst RESERVED_NAMES = new Set([\"Infinity\", \"NaN\", \"undefined\", \"eval\", \"arguments\"]);\n\n/** The local binding to use for a lucide export name. */\nfunction localName(name: string): string {\n return RESERVED_NAMES.has(name) ? `${name}Icon` : name;\n}\n\n/**\n * Walk a directory and collect every source file path.\n *\n * @param dir - Absolute directory to walk.\n * @param skip - Directory names to skip.\n * @returns Absolute file paths.\n */\nasync function collectSourceFiles(dir: string, skip: ReadonlySet<string>): Promise<string[]> {\n const out: string[] = [];\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return out;\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (skip.has(entry.name) || entry.name.startsWith(\".\")) continue;\n out.push(...(await collectSourceFiles(join(dir, entry.name), skip)));\n } else if (entry.isFile() && SOURCE_FILE.test(entry.name)) {\n out.push(join(dir, entry.name));\n }\n }\n return out;\n}\n\n/**\n * Generate a static icon registry from the slugs your source mentions.\n *\n * Removes the reason to reach for `lucide-react`'s `DynamicIcon`: the slugs an app\n * writes as literals become ordinary static imports, so the bundler keeps exactly\n * those icons and the browser makes **no** extra request for them. A slug that only\n * exists at runtime still works — `<Icon>` falls back to fetching one shard per\n * initial letter.\n *\n * The scan reads the source tree directly at `buildStart` rather than harvesting\n * modules as they pass through `transform`: relying on transform order would let\n * the virtual module load before every consumer had been seen, and the registry\n * would come out short on a cold dev start — silently, since the missing slugs\n * would still render via the lazy path.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestIcons } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestIcons()] });\n *\n * @example\n * // src/main.tsx\n * import { IconProvider } from \"tempest-react-sdk/icons\";\n * import { staticIcons } from \"tempest-react-sdk/icons/virtual\";\n *\n * <IconProvider registry={staticIcons}>\n * <App />\n * </IconProvider>\n */\nexport function tempestIcons(options: TempestIconsOptions = {}): TempestVitePlugin {\n const { dir = \"src\", include = [], skipDirs } = options;\n const known = new Set<string>(iconNames);\n const skip = new Set<string>(skipDirs ?? SKIP_DIRS);\n const slugs = new Set<string>(include.filter((slug) => known.has(slug)));\n let root = process.cwd();\n\n /**\n * Rescan the source tree, keeping the explicitly included slugs.\n *\n * @tempest-limits empty-catch — the scan races the editor and the file system:\n * a file listed a moment ago can be renamed, deleted, or held by another process\n * by the time it is read. An unreadable file contributes no slugs and the build\n * continues; failing the whole scan would break `vite dev` over a temp file that\n * no longer exists.\n */\n const rescan = async (): Promise<void> => {\n const files = await collectSourceFiles(resolve(root, dir), skip);\n for (const file of files) {\n try {\n const code = await readFile(file, \"utf8\");\n for (const slug of scanIconSlugs(code, known)) slugs.add(slug);\n } catch {\n /* empty */\n }\n }\n };\n\n return {\n name: \"tempest-icons\",\n enforce: \"pre\",\n\n configResolved(config: { root?: string }) {\n root = config.root ?? root;\n },\n\n async buildStart() {\n await rescan();\n },\n\n /**\n * Claim both spellings of the registry module.\n *\n * The subpath is a real file in the installed package, so returning the\n * virtual id here is what overrides the empty stub with the generated\n * table. Letting Vite resolve it normally would silently ship the stub.\n */\n resolveId(id: string) {\n return id === TEMPEST_ICONS_ID || id === TEMPEST_ICONS_VIRTUAL_ID ? RESOLVED_ID : null;\n },\n\n load(id: string) {\n return id === RESOLVED_ID ? buildIconsModule([...slugs], iconAliases) : null;\n },\n\n /**\n * Pick up a slug added while the dev server is running.\n *\n * A newly written `<Icon name=\"save\" />` would otherwise only render via\n * the lazy path until the next restart. Rescanning the changed file and\n * invalidating the virtual module keeps dev and build agreeing on what the\n * static registry contains.\n */\n async handleHotUpdate(ctx: {\n file: string;\n read: () => Promise<string>;\n server: { moduleGraph: { getModuleById: (id: string) => unknown } };\n }): Promise<unknown[] | undefined> {\n if (!SOURCE_FILE.test(ctx.file)) return undefined;\n const before = slugs.size;\n for (const slug of scanIconSlugs(await ctx.read(), known)) slugs.add(slug);\n if (slugs.size === before) return undefined;\n const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ID);\n return mod ? [mod] : undefined;\n },\n } as TempestVitePlugin;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e,t){if(typeof e!=`object`||!e)return null;let n=e[t];return typeof n==`number`&&Number.isFinite(n)?n:null}function t(e,t){if(typeof e!=`object`||!e)return null;let n=e[t];return typeof n==`string`?n:null}function n(e){return t(e,`kind`)??t(e,`mediaType`)}function r(e,t){return t===`all`||n(e)===t}function i(n){let r=null;n.forEach(e=>{t(e,`type`)===`transport`&&(r=t(e,`selectedCandidatePairId`)??r)});let i=null,a=null;n.forEach(n=>{if(t(n,`type`)!==`candidate-pair`)return;let o=e(n,`currentRoundTripTime`);o!==null&&(r!==null&&t(n,`id`)===r?i=o*1e3:a===null&&t(n,`state`)===`succeeded`&&(a=o*1e3))});let o=i??a;return o===null?null:Math.round(o)}function a(n={}){let a=n.kind??`video`,o=null,s=0,c={kbps:0,width:0,height:0,fps:0,rttMs:null};function l(n){let l=performance.now(),u=i(n),d=0,f=!1,p=0,m=0,h=0,g=0;if(n.forEach(n=>{if(t(n,`type`)!==`outbound-rtp`||!r(n,a))return;f=!0,d+=e(n,`bytesSent`)??0;let i=e(n,`frameWidth`)??0,o=e(n,`frameHeight`)??0,s=i*o;s<p||(p=s,m=i,h=o,g=Math.round(e(n,`framesPerSecond`)??0))}),!f)return c={...c,rttMs:u},c;let _=o===null?0:(l-s)/1e3,v=o===null?0:d-o,y=_>0&&v>0?Math.round(v*8/1e3/_):0;return o=d,s=l,c={kbps:y,width:m>0?m:c.width,height:h>0?h:c.height,fps:g>0?g:c.fps,rttMs:u},c}return{read:l,sample:async e=>l(await e.getStats()),reset:()=>{o=null,s=0,c={...c,kbps:0}}}}exports.createLinkStatsSampler=a,exports.readRoundTripMs=i;
|
|
2
|
+
//# sourceMappingURL=link-stats.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-stats.cjs","names":[],"sources":["../../src/webrtc/link-stats.ts"],"sourcesContent":["/**\n * One link's outbound picture, in the shape a call UI actually renders.\n *\n * Everything here is derived, not reported: WebRTC hands out cumulative\n * counters and a graph of candidate pairs, and turning that into\n * `\"1,2 Mbps · 42 ms · 1080p60\"` is the work {@link createLinkStatsSampler}\n * does.\n */\nexport interface LinkStats {\n /** Throughput since the previous sample. `0` on the first one — there is no delta yet. */\n kbps: number;\n /** Width of the largest stream being sent, or `0` before one is reported. */\n width: number;\n /** Height of the largest stream being sent, or `0` before one is reported. */\n height: number;\n /** Frame rate of the largest stream being sent, or `0` when the browser omits it. */\n fps: number;\n /** Round trip to the peer in milliseconds, or `null` before the first reading. */\n rttMs: number | null;\n}\n\n/** Which media a sampler counts. */\nexport type LinkStatsKind = \"video\" | \"audio\" | \"all\";\n\n/** Options for {@link createLinkStatsSampler}. */\nexport interface LinkStatsSamplerOptions {\n /**\n * Which media the throughput counts. Default `\"video\"`.\n *\n * Video is the default because it is what saturates an uplink — audio is an\n * order of magnitude cheaper, and mixing it in moves the number by less than\n * the noise between two samples. Use `\"all\"` when the figure is meant to be\n * the connection's real cost rather than the picture's.\n */\n kind?: LinkStatsKind;\n}\n\n/**\n * A sampler bound to one connection.\n *\n * Holds the previous byte counter and timestamp, which is the whole reason this\n * is an object rather than a function: the rate is a delta, so somebody has to\n * remember the last reading. One sampler per link — sharing one across peers\n * subtracts one connection's counter from another's and reports nonsense.\n */\nexport interface LinkStatsSampler {\n /**\n * Reduce a report you already have.\n *\n * @param report - A report from `RTCPeerConnection.getStats()`.\n * @returns The stats for this sample.\n */\n read: (report: RTCStatsReport) => LinkStats;\n /**\n * Fetch a report and reduce it.\n *\n * @param connection - The connection to sample.\n * @returns The stats for this sample.\n */\n sample: (connection: RTCPeerConnection) => Promise<LinkStats>;\n /**\n * Drop the baseline the rate is derived from.\n *\n * Call it after an ICE restart, a reconnect, or a pause — otherwise the next\n * sample divides the bytes of the whole gap by the whole gap and reports the\n * average of a period nobody is asking about. The next reading comes back at\n * `0` kbps and starts a fresh baseline; the resolution and round trip already\n * on screen are kept, so the badge does not blank out.\n */\n reset: () => void;\n}\n\nfunction numberField(entry: unknown, key: string): number | null {\n if (typeof entry !== \"object\" || entry === null) return null;\n const value: unknown = (entry as Record<string, unknown>)[key];\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\nfunction stringField(entry: unknown, key: string): string | null {\n if (typeof entry !== \"object\" || entry === null) return null;\n const value: unknown = (entry as Record<string, unknown>)[key];\n return typeof value === \"string\" ? value : null;\n}\n\n/**\n * Resolve the media an RTP entry carries.\n *\n * `kind` is the standard field and `mediaType` is what older Chrome reported;\n * both are still in the wild, and a sampler that reads only one of them\n * silently counts nothing on the browser that uses the other.\n */\nfunction entryKind(entry: unknown): string | null {\n return stringField(entry, \"kind\") ?? stringField(entry, \"mediaType\");\n}\n\nfunction matchesKind(entry: unknown, kind: LinkStatsKind): boolean {\n if (kind === \"all\") return true;\n return entryKind(entry) === kind;\n}\n\n/**\n * Read the round trip of the candidate pair actually carrying the link.\n *\n * A connection routinely keeps several viable pairs alive at once — host,\n * server-reflexive, relayed — and only one of them carries traffic. Reading the\n * first `succeeded` pair makes the number jump between paths that are not being\n * travelled: 8 ms on an idle host pair alternating with 180 ms on the TURN pair\n * doing the work. The pair the transport names in `selectedCandidatePairId` is\n * the one being used.\n *\n * A succeeded pair is kept as a fallback because not every browser fills that\n * field in — losing the reading entirely is worse than an occasionally\n * optimistic one.\n *\n * @param report - A report from `RTCPeerConnection.getStats()`.\n * @returns Round trip in milliseconds, rounded, or `null` when nothing reported\n * one — which is the normal state before the connection settles.\n *\n * @example\n * const rttMs = readRoundTripMs(await pc.getStats());\n */\nexport function readRoundTripMs(report: RTCStatsReport): number | null {\n let selectedId: string | null = null;\n report.forEach((entry: unknown) => {\n if (stringField(entry, \"type\") !== \"transport\") return;\n selectedId = stringField(entry, \"selectedCandidatePairId\") ?? selectedId;\n });\n\n let selected: number | null = null;\n let fallback: number | null = null;\n report.forEach((entry: unknown) => {\n if (stringField(entry, \"type\") !== \"candidate-pair\") return;\n const seconds = numberField(entry, \"currentRoundTripTime\");\n if (seconds === null) return;\n if (selectedId !== null && stringField(entry, \"id\") === selectedId)\n selected = seconds * 1000;\n else if (fallback === null && stringField(entry, \"state\") === \"succeeded\")\n fallback = seconds * 1000;\n });\n\n const value: number | null = selected ?? fallback;\n return value === null ? null : Math.round(value);\n}\n\n/**\n * Track one link's throughput, resolution and round trip across samples.\n *\n * Every rate here is a **delta**. `bytesSent` is cumulative since the connection\n * opened, so dividing it by the session length gives the historical average —\n * a number that only ever falls and never shows what is happening now. The\n * previous reading is kept on the sampler and subtracted, which is the part\n * every hand-rolled copy of this ends up rewriting.\n *\n * Bytes are summed across every matching sender, because a peer publishing a\n * camera and a screen at once occupies one uplink with both — and the uplink is\n * what runs out. Resolution and frame rate come from the **largest** stream by\n * area, which is the one that dominates that bandwidth and the one somebody\n * watching the call is looking at.\n *\n * @param options - See {@link LinkStatsSamplerOptions}.\n * @returns A sampler. Use one per `RTCPeerConnection`.\n *\n * @example\n * const sampler = createLinkStatsSampler();\n *\n * setInterval(async () => {\n * const stats = await sampler.sample(pc);\n * badge.textContent = `${stats.kbps} kbps · ${stats.rttMs ?? \"—\"} ms`;\n * }, 2000);\n */\nexport function createLinkStatsSampler(options: LinkStatsSamplerOptions = {}): LinkStatsSampler {\n const kind: LinkStatsKind = options.kind ?? \"video\";\n let lastBytes: number | null = null;\n let lastSampleAt = 0;\n let last: LinkStats = { kbps: 0, width: 0, height: 0, fps: 0, rttMs: null };\n\n function read(report: RTCStatsReport): LinkStats {\n const now = performance.now();\n const rttMs = readRoundTripMs(report);\n\n let bytes = 0;\n let sawSender = false;\n let bestArea = 0;\n let width = 0;\n let height = 0;\n let fps = 0;\n\n report.forEach((entry: unknown) => {\n if (stringField(entry, \"type\") !== \"outbound-rtp\") return;\n if (!matchesKind(entry, kind)) return;\n sawSender = true;\n bytes += numberField(entry, \"bytesSent\") ?? 0;\n\n const entryWidth = numberField(entry, \"frameWidth\") ?? 0;\n const entryHeight = numberField(entry, \"frameHeight\") ?? 0;\n const area = entryWidth * entryHeight;\n if (area < bestArea) return;\n bestArea = area;\n width = entryWidth;\n height = entryHeight;\n fps = Math.round(numberField(entry, \"framesPerSecond\") ?? 0);\n });\n\n if (!sawSender) {\n last = { ...last, rttMs };\n return last;\n }\n\n const elapsed = lastBytes === null ? 0 : (now - lastSampleAt) / 1000;\n const delta = lastBytes === null ? 0 : bytes - lastBytes;\n const kbps = elapsed > 0 && delta > 0 ? Math.round((delta * 8) / 1000 / elapsed) : 0;\n\n lastBytes = bytes;\n lastSampleAt = now;\n last = {\n kbps,\n width: width > 0 ? width : last.width,\n height: height > 0 ? height : last.height,\n fps: fps > 0 ? fps : last.fps,\n rttMs,\n };\n return last;\n }\n\n return {\n read,\n sample: async (connection: RTCPeerConnection): Promise<LinkStats> =>\n read(await connection.getStats()),\n reset: (): void => {\n lastBytes = null;\n lastSampleAt = 0;\n last = { ...last, kbps: 0 };\n },\n };\n}\n"],"mappings":"AAwEA,SAAS,EAAY,EAAgB,EAA4B,CAC7D,GAAI,OAAO,GAAU,WAAY,EAAgB,OAAO,KACxD,IAAM,EAAkB,EAAkC,GAC1D,OAAO,OAAO,GAAU,UAAY,OAAO,SAAS,CAAK,EAAI,EAAQ,IACzE,CAEA,SAAS,EAAY,EAAgB,EAA4B,CAC7D,GAAI,OAAO,GAAU,WAAY,EAAgB,OAAO,KACxD,IAAM,EAAkB,EAAkC,GAC1D,OAAO,OAAO,GAAU,SAAW,EAAQ,IAC/C,CASA,SAAS,EAAU,EAA+B,CAC9C,OAAO,EAAY,EAAO,MAAM,GAAK,EAAY,EAAO,WAAW,CACvE,CAEA,SAAS,EAAY,EAAgB,EAA8B,CAE/D,OADI,IAAS,OACN,EAAU,CAAK,IAAM,CAChC,CAuBA,SAAgB,EAAgB,EAAuC,CACnE,IAAI,EAA4B,KAChC,EAAO,QAAS,GAAmB,CAC3B,EAAY,EAAO,MAAM,IAAM,cACnC,EAAa,EAAY,EAAO,yBAAyB,GAAK,EAClE,CAAC,EAED,IAAI,EAA0B,KAC1B,EAA0B,KAC9B,EAAO,QAAS,GAAmB,CAC/B,GAAI,EAAY,EAAO,MAAM,IAAM,iBAAkB,OACrD,IAAM,EAAU,EAAY,EAAO,sBAAsB,EACrD,IAAY,OACZ,IAAe,MAAQ,EAAY,EAAO,IAAI,IAAM,EACpD,EAAW,EAAU,IAChB,IAAa,MAAQ,EAAY,EAAO,OAAO,IAAM,cAC1D,EAAW,EAAU,KAC7B,CAAC,EAED,IAAM,EAAuB,GAAY,EACzC,OAAO,IAAU,KAAO,KAAO,KAAK,MAAM,CAAK,CACnD,CA4BA,SAAgB,EAAuB,EAAmC,CAAC,EAAqB,CAC5F,IAAM,EAAsB,EAAQ,MAAQ,QACxC,EAA2B,KAC3B,EAAe,EACf,EAAkB,CAAE,KAAM,EAAG,MAAO,EAAG,OAAQ,EAAG,IAAK,EAAG,MAAO,IAAK,EAE1E,SAAS,EAAK,EAAmC,CAC7C,IAAM,EAAM,YAAY,IAAI,EACtB,EAAQ,EAAgB,CAAM,EAEhC,EAAQ,EACR,EAAY,GACZ,EAAW,EACX,EAAQ,EACR,EAAS,EACT,EAAM,EAkBV,GAhBA,EAAO,QAAS,GAAmB,CAE/B,GADI,EAAY,EAAO,MAAM,IAAM,gBAC/B,CAAC,EAAY,EAAO,CAAI,EAAG,OAC/B,EAAY,GACZ,GAAS,EAAY,EAAO,WAAW,GAAK,EAE5C,IAAM,EAAa,EAAY,EAAO,YAAY,GAAK,EACjD,EAAc,EAAY,EAAO,aAAa,GAAK,EACnD,EAAO,EAAa,EACtB,EAAO,IACX,EAAW,EACX,EAAQ,EACR,EAAS,EACT,EAAM,KAAK,MAAM,EAAY,EAAO,iBAAiB,GAAK,CAAC,EAC/D,CAAC,EAEG,CAAC,EAED,MADA,GAAO,CAAE,GAAG,EAAM,OAAM,EACjB,EAGX,IAAM,EAAU,IAAc,KAAO,GAAK,EAAM,GAAgB,IAC1D,EAAQ,IAAc,KAAO,EAAI,EAAQ,EACzC,EAAO,EAAU,GAAK,EAAQ,EAAI,KAAK,MAAO,EAAQ,EAAK,IAAO,CAAO,EAAI,EAWnF,MATA,GAAY,EACZ,EAAe,EACf,EAAO,CACH,OACA,MAAO,EAAQ,EAAI,EAAQ,EAAK,MAChC,OAAQ,EAAS,EAAI,EAAS,EAAK,OACnC,IAAK,EAAM,EAAI,EAAM,EAAK,IAC1B,OACJ,EACO,CACX,CAEA,MAAO,CACH,OACA,OAAQ,KAAO,IACX,EAAK,MAAM,EAAW,SAAS,CAAC,EACpC,UAAmB,CACf,EAAY,KACZ,EAAe,EACf,EAAO,CAAE,GAAG,EAAM,KAAM,CAAE,CAC9B,CACJ,CACJ"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//#region src/webrtc/link-stats.ts
|
|
2
|
+
function e(e, t) {
|
|
3
|
+
if (typeof e != "object" || !e) return null;
|
|
4
|
+
let n = e[t];
|
|
5
|
+
return typeof n == "number" && Number.isFinite(n) ? n : null;
|
|
6
|
+
}
|
|
7
|
+
function t(e, t) {
|
|
8
|
+
if (typeof e != "object" || !e) return null;
|
|
9
|
+
let n = e[t];
|
|
10
|
+
return typeof n == "string" ? n : null;
|
|
11
|
+
}
|
|
12
|
+
function n(e) {
|
|
13
|
+
return t(e, "kind") ?? t(e, "mediaType");
|
|
14
|
+
}
|
|
15
|
+
function r(e, t) {
|
|
16
|
+
return t === "all" || n(e) === t;
|
|
17
|
+
}
|
|
18
|
+
function i(n) {
|
|
19
|
+
let r = null;
|
|
20
|
+
n.forEach((e) => {
|
|
21
|
+
t(e, "type") === "transport" && (r = t(e, "selectedCandidatePairId") ?? r);
|
|
22
|
+
});
|
|
23
|
+
let i = null, a = null;
|
|
24
|
+
n.forEach((n) => {
|
|
25
|
+
if (t(n, "type") !== "candidate-pair") return;
|
|
26
|
+
let o = e(n, "currentRoundTripTime");
|
|
27
|
+
o !== null && (r !== null && t(n, "id") === r ? i = o * 1e3 : a === null && t(n, "state") === "succeeded" && (a = o * 1e3));
|
|
28
|
+
});
|
|
29
|
+
let o = i ?? a;
|
|
30
|
+
return o === null ? null : Math.round(o);
|
|
31
|
+
}
|
|
32
|
+
function a(n = {}) {
|
|
33
|
+
let a = n.kind ?? "video", o = null, s = 0, c = {
|
|
34
|
+
kbps: 0,
|
|
35
|
+
width: 0,
|
|
36
|
+
height: 0,
|
|
37
|
+
fps: 0,
|
|
38
|
+
rttMs: null
|
|
39
|
+
};
|
|
40
|
+
function l(n) {
|
|
41
|
+
let l = performance.now(), u = i(n), d = 0, f = !1, p = 0, m = 0, h = 0, g = 0;
|
|
42
|
+
if (n.forEach((n) => {
|
|
43
|
+
if (t(n, "type") !== "outbound-rtp" || !r(n, a)) return;
|
|
44
|
+
f = !0, d += e(n, "bytesSent") ?? 0;
|
|
45
|
+
let i = e(n, "frameWidth") ?? 0, o = e(n, "frameHeight") ?? 0, s = i * o;
|
|
46
|
+
s < p || (p = s, m = i, h = o, g = Math.round(e(n, "framesPerSecond") ?? 0));
|
|
47
|
+
}), !f) return c = {
|
|
48
|
+
...c,
|
|
49
|
+
rttMs: u
|
|
50
|
+
}, c;
|
|
51
|
+
let _ = o === null ? 0 : (l - s) / 1e3, v = o === null ? 0 : d - o, y = _ > 0 && v > 0 ? Math.round(v * 8 / 1e3 / _) : 0;
|
|
52
|
+
return o = d, s = l, c = {
|
|
53
|
+
kbps: y,
|
|
54
|
+
width: m > 0 ? m : c.width,
|
|
55
|
+
height: h > 0 ? h : c.height,
|
|
56
|
+
fps: g > 0 ? g : c.fps,
|
|
57
|
+
rttMs: u
|
|
58
|
+
}, c;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
read: l,
|
|
62
|
+
sample: async (e) => l(await e.getStats()),
|
|
63
|
+
reset: () => {
|
|
64
|
+
o = null, s = 0, c = {
|
|
65
|
+
...c,
|
|
66
|
+
kbps: 0
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { a as createLinkStatsSampler, i as readRoundTripMs };
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=link-stats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-stats.js","names":[],"sources":["../../src/webrtc/link-stats.ts"],"sourcesContent":["/**\n * One link's outbound picture, in the shape a call UI actually renders.\n *\n * Everything here is derived, not reported: WebRTC hands out cumulative\n * counters and a graph of candidate pairs, and turning that into\n * `\"1,2 Mbps · 42 ms · 1080p60\"` is the work {@link createLinkStatsSampler}\n * does.\n */\nexport interface LinkStats {\n /** Throughput since the previous sample. `0` on the first one — there is no delta yet. */\n kbps: number;\n /** Width of the largest stream being sent, or `0` before one is reported. */\n width: number;\n /** Height of the largest stream being sent, or `0` before one is reported. */\n height: number;\n /** Frame rate of the largest stream being sent, or `0` when the browser omits it. */\n fps: number;\n /** Round trip to the peer in milliseconds, or `null` before the first reading. */\n rttMs: number | null;\n}\n\n/** Which media a sampler counts. */\nexport type LinkStatsKind = \"video\" | \"audio\" | \"all\";\n\n/** Options for {@link createLinkStatsSampler}. */\nexport interface LinkStatsSamplerOptions {\n /**\n * Which media the throughput counts. Default `\"video\"`.\n *\n * Video is the default because it is what saturates an uplink — audio is an\n * order of magnitude cheaper, and mixing it in moves the number by less than\n * the noise between two samples. Use `\"all\"` when the figure is meant to be\n * the connection's real cost rather than the picture's.\n */\n kind?: LinkStatsKind;\n}\n\n/**\n * A sampler bound to one connection.\n *\n * Holds the previous byte counter and timestamp, which is the whole reason this\n * is an object rather than a function: the rate is a delta, so somebody has to\n * remember the last reading. One sampler per link — sharing one across peers\n * subtracts one connection's counter from another's and reports nonsense.\n */\nexport interface LinkStatsSampler {\n /**\n * Reduce a report you already have.\n *\n * @param report - A report from `RTCPeerConnection.getStats()`.\n * @returns The stats for this sample.\n */\n read: (report: RTCStatsReport) => LinkStats;\n /**\n * Fetch a report and reduce it.\n *\n * @param connection - The connection to sample.\n * @returns The stats for this sample.\n */\n sample: (connection: RTCPeerConnection) => Promise<LinkStats>;\n /**\n * Drop the baseline the rate is derived from.\n *\n * Call it after an ICE restart, a reconnect, or a pause — otherwise the next\n * sample divides the bytes of the whole gap by the whole gap and reports the\n * average of a period nobody is asking about. The next reading comes back at\n * `0` kbps and starts a fresh baseline; the resolution and round trip already\n * on screen are kept, so the badge does not blank out.\n */\n reset: () => void;\n}\n\nfunction numberField(entry: unknown, key: string): number | null {\n if (typeof entry !== \"object\" || entry === null) return null;\n const value: unknown = (entry as Record<string, unknown>)[key];\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\nfunction stringField(entry: unknown, key: string): string | null {\n if (typeof entry !== \"object\" || entry === null) return null;\n const value: unknown = (entry as Record<string, unknown>)[key];\n return typeof value === \"string\" ? value : null;\n}\n\n/**\n * Resolve the media an RTP entry carries.\n *\n * `kind` is the standard field and `mediaType` is what older Chrome reported;\n * both are still in the wild, and a sampler that reads only one of them\n * silently counts nothing on the browser that uses the other.\n */\nfunction entryKind(entry: unknown): string | null {\n return stringField(entry, \"kind\") ?? stringField(entry, \"mediaType\");\n}\n\nfunction matchesKind(entry: unknown, kind: LinkStatsKind): boolean {\n if (kind === \"all\") return true;\n return entryKind(entry) === kind;\n}\n\n/**\n * Read the round trip of the candidate pair actually carrying the link.\n *\n * A connection routinely keeps several viable pairs alive at once — host,\n * server-reflexive, relayed — and only one of them carries traffic. Reading the\n * first `succeeded` pair makes the number jump between paths that are not being\n * travelled: 8 ms on an idle host pair alternating with 180 ms on the TURN pair\n * doing the work. The pair the transport names in `selectedCandidatePairId` is\n * the one being used.\n *\n * A succeeded pair is kept as a fallback because not every browser fills that\n * field in — losing the reading entirely is worse than an occasionally\n * optimistic one.\n *\n * @param report - A report from `RTCPeerConnection.getStats()`.\n * @returns Round trip in milliseconds, rounded, or `null` when nothing reported\n * one — which is the normal state before the connection settles.\n *\n * @example\n * const rttMs = readRoundTripMs(await pc.getStats());\n */\nexport function readRoundTripMs(report: RTCStatsReport): number | null {\n let selectedId: string | null = null;\n report.forEach((entry: unknown) => {\n if (stringField(entry, \"type\") !== \"transport\") return;\n selectedId = stringField(entry, \"selectedCandidatePairId\") ?? selectedId;\n });\n\n let selected: number | null = null;\n let fallback: number | null = null;\n report.forEach((entry: unknown) => {\n if (stringField(entry, \"type\") !== \"candidate-pair\") return;\n const seconds = numberField(entry, \"currentRoundTripTime\");\n if (seconds === null) return;\n if (selectedId !== null && stringField(entry, \"id\") === selectedId)\n selected = seconds * 1000;\n else if (fallback === null && stringField(entry, \"state\") === \"succeeded\")\n fallback = seconds * 1000;\n });\n\n const value: number | null = selected ?? fallback;\n return value === null ? null : Math.round(value);\n}\n\n/**\n * Track one link's throughput, resolution and round trip across samples.\n *\n * Every rate here is a **delta**. `bytesSent` is cumulative since the connection\n * opened, so dividing it by the session length gives the historical average —\n * a number that only ever falls and never shows what is happening now. The\n * previous reading is kept on the sampler and subtracted, which is the part\n * every hand-rolled copy of this ends up rewriting.\n *\n * Bytes are summed across every matching sender, because a peer publishing a\n * camera and a screen at once occupies one uplink with both — and the uplink is\n * what runs out. Resolution and frame rate come from the **largest** stream by\n * area, which is the one that dominates that bandwidth and the one somebody\n * watching the call is looking at.\n *\n * @param options - See {@link LinkStatsSamplerOptions}.\n * @returns A sampler. Use one per `RTCPeerConnection`.\n *\n * @example\n * const sampler = createLinkStatsSampler();\n *\n * setInterval(async () => {\n * const stats = await sampler.sample(pc);\n * badge.textContent = `${stats.kbps} kbps · ${stats.rttMs ?? \"—\"} ms`;\n * }, 2000);\n */\nexport function createLinkStatsSampler(options: LinkStatsSamplerOptions = {}): LinkStatsSampler {\n const kind: LinkStatsKind = options.kind ?? \"video\";\n let lastBytes: number | null = null;\n let lastSampleAt = 0;\n let last: LinkStats = { kbps: 0, width: 0, height: 0, fps: 0, rttMs: null };\n\n function read(report: RTCStatsReport): LinkStats {\n const now = performance.now();\n const rttMs = readRoundTripMs(report);\n\n let bytes = 0;\n let sawSender = false;\n let bestArea = 0;\n let width = 0;\n let height = 0;\n let fps = 0;\n\n report.forEach((entry: unknown) => {\n if (stringField(entry, \"type\") !== \"outbound-rtp\") return;\n if (!matchesKind(entry, kind)) return;\n sawSender = true;\n bytes += numberField(entry, \"bytesSent\") ?? 0;\n\n const entryWidth = numberField(entry, \"frameWidth\") ?? 0;\n const entryHeight = numberField(entry, \"frameHeight\") ?? 0;\n const area = entryWidth * entryHeight;\n if (area < bestArea) return;\n bestArea = area;\n width = entryWidth;\n height = entryHeight;\n fps = Math.round(numberField(entry, \"framesPerSecond\") ?? 0);\n });\n\n if (!sawSender) {\n last = { ...last, rttMs };\n return last;\n }\n\n const elapsed = lastBytes === null ? 0 : (now - lastSampleAt) / 1000;\n const delta = lastBytes === null ? 0 : bytes - lastBytes;\n const kbps = elapsed > 0 && delta > 0 ? Math.round((delta * 8) / 1000 / elapsed) : 0;\n\n lastBytes = bytes;\n lastSampleAt = now;\n last = {\n kbps,\n width: width > 0 ? width : last.width,\n height: height > 0 ? height : last.height,\n fps: fps > 0 ? fps : last.fps,\n rttMs,\n };\n return last;\n }\n\n return {\n read,\n sample: async (connection: RTCPeerConnection): Promise<LinkStats> =>\n read(await connection.getStats()),\n reset: (): void => {\n lastBytes = null;\n lastSampleAt = 0;\n last = { ...last, kbps: 0 };\n },\n };\n}\n"],"mappings":";AAwEA,SAAS,EAAY,GAAgB,GAA4B;CAC7D,IAAI,OAAO,KAAU,aAAY,GAAgB,OAAO;CACxD,IAAM,IAAkB,EAAkC;CAC1D,OAAO,OAAO,KAAU,YAAY,OAAO,SAAS,CAAK,IAAI,IAAQ;AACzE;AAEA,SAAS,EAAY,GAAgB,GAA4B;CAC7D,IAAI,OAAO,KAAU,aAAY,GAAgB,OAAO;CACxD,IAAM,IAAkB,EAAkC;CAC1D,OAAO,OAAO,KAAU,WAAW,IAAQ;AAC/C;AASA,SAAS,EAAU,GAA+B;CAC9C,OAAO,EAAY,GAAO,MAAM,KAAK,EAAY,GAAO,WAAW;AACvE;AAEA,SAAS,EAAY,GAAgB,GAA8B;CAE/D,OADI,MAAS,SACN,EAAU,CAAK,MAAM;AAChC;AAuBA,SAAgB,EAAgB,GAAuC;CACnE,IAAI,IAA4B;CAChC,EAAO,SAAS,MAAmB;EAC3B,EAAY,GAAO,MAAM,MAAM,gBACnC,IAAa,EAAY,GAAO,yBAAyB,KAAK;CAClE,CAAC;CAED,IAAI,IAA0B,MAC1B,IAA0B;CAC9B,EAAO,SAAS,MAAmB;EAC/B,IAAI,EAAY,GAAO,MAAM,MAAM,kBAAkB;EACrD,IAAM,IAAU,EAAY,GAAO,sBAAsB;EACrD,MAAY,SACZ,MAAe,QAAQ,EAAY,GAAO,IAAI,MAAM,IACpD,IAAW,IAAU,MAChB,MAAa,QAAQ,EAAY,GAAO,OAAO,MAAM,gBAC1D,IAAW,IAAU;CAC7B,CAAC;CAED,IAAM,IAAuB,KAAY;CACzC,OAAO,MAAU,OAAO,OAAO,KAAK,MAAM,CAAK;AACnD;AA4BA,SAAgB,EAAuB,IAAmC,CAAC,GAAqB;CAC5F,IAAM,IAAsB,EAAQ,QAAQ,SACxC,IAA2B,MAC3B,IAAe,GACf,IAAkB;EAAE,MAAM;EAAG,OAAO;EAAG,QAAQ;EAAG,KAAK;EAAG,OAAO;CAAK;CAE1E,SAAS,EAAK,GAAmC;EAC7C,IAAM,IAAM,YAAY,IAAI,GACtB,IAAQ,EAAgB,CAAM,GAEhC,IAAQ,GACR,IAAY,IACZ,IAAW,GACX,IAAQ,GACR,IAAS,GACT,IAAM;EAkBV,IAhBA,EAAO,SAAS,MAAmB;GAE/B,IADI,EAAY,GAAO,MAAM,MAAM,kBAC/B,CAAC,EAAY,GAAO,CAAI,GAAG;GAE/B,AADA,IAAY,IACZ,KAAS,EAAY,GAAO,WAAW,KAAK;GAE5C,IAAM,IAAa,EAAY,GAAO,YAAY,KAAK,GACjD,IAAc,EAAY,GAAO,aAAa,KAAK,GACnD,IAAO,IAAa;GACtB,IAAO,MACX,IAAW,GACX,IAAQ,GACR,IAAS,GACT,IAAM,KAAK,MAAM,EAAY,GAAO,iBAAiB,KAAK,CAAC;EAC/D,CAAC,GAEG,CAAC,GAED,OADA,IAAO;GAAE,GAAG;GAAM;EAAM,GACjB;EAGX,IAAM,IAAU,MAAc,OAAO,KAAK,IAAM,KAAgB,KAC1D,IAAQ,MAAc,OAAO,IAAI,IAAQ,GACzC,IAAO,IAAU,KAAK,IAAQ,IAAI,KAAK,MAAO,IAAQ,IAAK,MAAO,CAAO,IAAI;EAWnF,OATA,IAAY,GACZ,IAAe,GACf,IAAO;GACH;GACA,OAAO,IAAQ,IAAI,IAAQ,EAAK;GAChC,QAAQ,IAAS,IAAI,IAAS,EAAK;GACnC,KAAK,IAAM,IAAI,IAAM,EAAK;GAC1B;EACJ,GACO;CACX;CAEA,OAAO;EACH;EACA,QAAQ,OAAO,MACX,EAAK,MAAM,EAAW,SAAS,CAAC;EACpC,aAAmB;GAGf,AAFA,IAAY,MACZ,IAAe,GACf,IAAO;IAAE,GAAG;IAAM,MAAM;GAAE;EAC9B;CACJ;AACJ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./sender-bitrate.cjs");var t=6e3,n=300,r=900;function i(e,r){let i=e.video??{};if(r<=1)return e;let a=Object.values(i).reduce((e,t)=>e+(t??0),0);if(a===0)return e;let o=(e.uplinkBudgetKbps??t)/r;if(a<=o)return e;let s=e.minVideoKbps??n,c=o/a,l={};for(let[e,t]of Object.entries(i))l[e]=t===null?null:Math.max(s,Math.round(t*c));return{...e,video:l}}function a(e,t){let n=e.degradationPreference;if(n!==`maintain-framerate`)return n;let i=Object.values(t.video??{}).filter(e=>e!==null);return i.length===0?n:Math.max(...i)>=(e.fluidFloorKbps??r)?`maintain-framerate`:`maintain-resolution`}function o(e,t,n){let r=e.getParameters();if(t!==void 0&&(r.degradationPreference=t),n!==void 0)for(let e of r.encodings??[])e.maxFramerate=n;return r}async function s(t,n,r,i){for(let[a,s]of n.entries()){let n=t[a]?.sender;if(!n)continue;if(s.kind===`audio`){let t=r.audio?.[s.name];t!==void 0&&await e.setSenderBitrate(n,t);continue}let c=r.video?.[s.name];if(c!==void 0&&await e.setSenderBitrate(n,c===null?null:c*1e3),i!==void 0||r.maxFramerate!==void 0)try{await n.setParameters(o(n,i,r.maxFramerate))}catch{try{await n.setParameters(o(n,void 0,r.maxFramerate))}catch{}}}}exports.applyQualityToLink=s,exports.resolveDegradation=a,exports.scaleForRoom=i;
|
|
2
|
+
//# sourceMappingURL=mesh-quality.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mesh-quality.cjs","names":[],"sources":["../../src/webrtc/mesh-quality.ts"],"sourcesContent":["import { setSenderBitrate } from \"./sender-bitrate\";\nimport type { MeshQuality, MeshSlot } from \"./mesh-types\";\n\n/** Uplink assumed available when the caller names no budget, in kbps. */\nconst DEFAULT_UPLINK_BUDGET_KBPS = 6000;\n\n/** Floor a video slot keeps after the division, in kbps. */\nconst DEFAULT_MIN_VIDEO_KBPS = 300;\n\n/** Budget below which `maintain-framerate` stops being worth holding, in kbps. */\nconst DEFAULT_FLUID_FLOOR_KBPS = 900;\n\n/**\n * Divide the video caps by the size of the room.\n *\n * A mesh sends one copy of everything per participant, so the uplink is the\n * shared resource and the caps are what compete for it. Nothing is divided while\n * the caller is alone with one peer — that is the case the largest sizes exist\n * for, and dividing a budget that is not being shared would make them\n * unreachable in the only call where they fit.\n *\n * The floor is what keeps the division honest: without it a busy room allocates\n * tens of kbps per stream, and everybody loses the picture instead of the excess\n * giving way.\n *\n * @param quality - The caps as asked for.\n * @param peers - How many links are live.\n * @returns The caps to actually apply. The input is never mutated, so a room\n * that empties out climbs back to what was asked for.\n */\nexport function scaleForRoom(quality: MeshQuality, peers: number): MeshQuality {\n const video = quality.video ?? {};\n if (peers <= 1) return quality;\n\n const asked = Object.values(video).reduce<number>((sum, cap) => sum + (cap ?? 0), 0);\n if (asked === 0) return quality;\n\n const perPeer = (quality.uplinkBudgetKbps ?? DEFAULT_UPLINK_BUDGET_KBPS) / peers;\n if (asked <= perPeer) return quality;\n\n const floor = quality.minVideoKbps ?? DEFAULT_MIN_VIDEO_KBPS;\n const factor = perPeer / asked;\n const scaled: Record<string, number | null> = {};\n for (const [slot, cap] of Object.entries(video)) {\n scaled[slot] = cap === null ? null : Math.max(floor, Math.round(cap * factor));\n }\n return { ...quality, video: scaled };\n}\n\n/**\n * Decide what the encoder gives up, letting physics override the preference.\n *\n * `maintain-framerate` is honoured while there are bits enough for the frames to\n * be worth keeping. Once the room's division has taken the budget below the\n * fluid floor, holding the rate halves what each frame receives and the picture\n * is worse than the lower rate it replaced — so below that line the preference\n * is overridden rather than obeyed.\n *\n * @param quality - What was asked for.\n * @param effective - What the room's share actually allows.\n * @returns The degradation preference to write onto the senders, or `undefined`\n * when the caller expressed no preference.\n */\nexport function resolveDegradation(\n quality: MeshQuality,\n effective: MeshQuality,\n): RTCDegradationPreference | undefined {\n const asked = quality.degradationPreference;\n if (asked !== \"maintain-framerate\") return asked;\n\n const caps = Object.values(effective.video ?? {}).filter((cap): cap is number => cap !== null);\n if (caps.length === 0) return asked;\n\n const budget = Math.max(...caps);\n return budget >= (quality.fluidFloorKbps ?? DEFAULT_FLUID_FLOOR_KBPS)\n ? \"maintain-framerate\"\n : \"maintain-resolution\";\n}\n\n/**\n * Read a video sender's parameters back with the motion settings written on.\n *\n * Read fresh on every call because `setParameters` only accepts the object the\n * **same** sender's `getParameters` returned, so a rejected attempt cannot be\n * retried with the object that was rejected.\n *\n * @param sender - The video sender to read from.\n * @param degradation - What to give up first, or `undefined` to leave it alone.\n * @param fps - Frame ceiling, or `undefined` to leave it alone.\n * @returns Parameters ready to be written back to `sender`.\n */\nfunction videoParameters(\n sender: RTCRtpSender,\n degradation: RTCDegradationPreference | undefined,\n fps: number | undefined,\n): RTCRtpSendParameters {\n const params = sender.getParameters();\n if (degradation !== undefined) params.degradationPreference = degradation;\n if (fps !== undefined) {\n for (const encoding of params.encodings ?? []) encoding.maxFramerate = fps;\n }\n return params;\n}\n\n/**\n * Write the quality settings onto one link's senders.\n *\n * `degradationPreference` goes only on video: it describes trading resolution\n * against frame rate, which an audio sender has no analogue for, and some\n * browsers reject it outright there.\n *\n * The retry without `degradationPreference` is for Firefox, which rejects the\n * member entirely — sending both together would lose the frame-rate cap to an\n * objection about something else.\n *\n * @param transceivers - The link's transceivers, in slot order.\n * @param slots - The slot list those transceivers were allocated from.\n * @param effective - Caps after the room's division.\n * @param degradation - Already resolved against the fluid floor.\n */\nexport async function applyQualityToLink(\n transceivers: readonly RTCRtpTransceiver[],\n slots: readonly MeshSlot[],\n effective: MeshQuality,\n degradation: RTCDegradationPreference | undefined,\n): Promise<void> {\n for (const [index, slot] of slots.entries()) {\n const sender = transceivers[index]?.sender;\n if (!sender) continue;\n\n if (slot.kind === \"audio\") {\n const bps = effective.audio?.[slot.name];\n if (bps !== undefined) await setSenderBitrate(sender, bps);\n continue;\n }\n\n const kbps = effective.video?.[slot.name];\n if (kbps !== undefined) await setSenderBitrate(sender, kbps === null ? null : kbps * 1000);\n\n if (degradation === undefined && effective.maxFramerate === undefined) continue;\n try {\n await sender.setParameters(\n videoParameters(sender, degradation, effective.maxFramerate),\n );\n } catch {\n try {\n await sender.setParameters(\n videoParameters(sender, undefined, effective.maxFramerate),\n );\n } catch {\n /* the sender refused both; the bitrate cap above still applies */\n }\n }\n }\n}\n"],"mappings":"wCAIA,IAAM,EAA6B,IAG7B,EAAyB,IAGzB,EAA2B,IAoBjC,SAAgB,EAAa,EAAsB,EAA4B,CAC3E,IAAM,EAAQ,EAAQ,OAAS,CAAC,EAChC,GAAI,GAAS,EAAG,OAAO,EAEvB,IAAM,EAAQ,OAAO,OAAO,CAAK,CAAC,CAAC,QAAgB,EAAK,IAAQ,GAAO,GAAO,GAAI,CAAC,EACnF,GAAI,IAAU,EAAG,OAAO,EAExB,IAAM,GAAW,EAAQ,kBAAoB,GAA8B,EAC3E,GAAI,GAAS,EAAS,OAAO,EAE7B,IAAM,EAAQ,EAAQ,cAAgB,EAChC,EAAS,EAAU,EACnB,EAAwC,CAAC,EAC/C,IAAK,GAAM,CAAC,EAAM,KAAQ,OAAO,QAAQ,CAAK,EAC1C,EAAO,GAAQ,IAAQ,KAAO,KAAO,KAAK,IAAI,EAAO,KAAK,MAAM,EAAM,CAAM,CAAC,EAEjF,MAAO,CAAE,GAAG,EAAS,MAAO,CAAO,CACvC,CAgBA,SAAgB,EACZ,EACA,EACoC,CACpC,IAAM,EAAQ,EAAQ,sBACtB,GAAI,IAAU,qBAAsB,OAAO,EAE3C,IAAM,EAAO,OAAO,OAAO,EAAU,OAAS,CAAC,CAAC,CAAC,CAAC,OAAQ,GAAuB,IAAQ,IAAI,EAI7F,OAHI,EAAK,SAAW,EAAU,EAEf,KAAK,IAAI,GAAG,CACpB,IAAW,EAAQ,gBAAkB,GACtC,qBACA,qBACV,CAcA,SAAS,EACL,EACA,EACA,EACoB,CACpB,IAAM,EAAS,EAAO,cAAc,EAEpC,GADI,IAAgB,IAAA,KAAW,EAAO,sBAAwB,GAC1D,IAAQ,IAAA,GACR,IAAK,IAAM,KAAY,EAAO,WAAa,CAAC,EAAG,EAAS,aAAe,EAE3E,OAAO,CACX,CAkBA,eAAsB,EAClB,EACA,EACA,EACA,EACa,CACb,IAAK,GAAM,CAAC,EAAO,KAAS,EAAM,QAAQ,EAAG,CACzC,IAAM,EAAS,EAAa,EAAM,EAAE,OACpC,GAAI,CAAC,EAAQ,SAEb,GAAI,EAAK,OAAS,QAAS,CACvB,IAAM,EAAM,EAAU,QAAQ,EAAK,MAC/B,IAAQ,IAAA,IAAW,MAAM,EAAA,iBAAiB,EAAQ,CAAG,EACzD,QACJ,CAEA,IAAM,EAAO,EAAU,QAAQ,EAAK,MACpC,GAAI,IAAS,IAAA,IAAW,MAAM,EAAA,iBAAiB,EAAQ,IAAS,KAAO,KAAO,EAAO,GAAI,EAErF,IAAgB,IAAA,IAAa,EAAU,eAAiB,IAAA,GAC5D,GAAI,CACA,MAAM,EAAO,cACT,EAAgB,EAAQ,EAAa,EAAU,YAAY,CAC/D,CACJ,MAAQ,CACJ,GAAI,CACA,MAAM,EAAO,cACT,EAAgB,EAAQ,IAAA,GAAW,EAAU,YAAY,CAC7D,CACJ,MAAQ,CAER,CACJ,CACJ,CACJ"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { setSenderBitrate as e } from "./sender-bitrate.js";
|
|
2
|
+
//#region src/webrtc/mesh-quality.ts
|
|
3
|
+
var t = 6e3, n = 300, r = 900;
|
|
4
|
+
function i(e, r) {
|
|
5
|
+
let i = e.video ?? {};
|
|
6
|
+
if (r <= 1) return e;
|
|
7
|
+
let a = Object.values(i).reduce((e, t) => e + (t ?? 0), 0);
|
|
8
|
+
if (a === 0) return e;
|
|
9
|
+
let o = (e.uplinkBudgetKbps ?? t) / r;
|
|
10
|
+
if (a <= o) return e;
|
|
11
|
+
let s = e.minVideoKbps ?? n, c = o / a, l = {};
|
|
12
|
+
for (let [e, t] of Object.entries(i)) l[e] = t === null ? null : Math.max(s, Math.round(t * c));
|
|
13
|
+
return {
|
|
14
|
+
...e,
|
|
15
|
+
video: l
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function a(e, t) {
|
|
19
|
+
let n = e.degradationPreference;
|
|
20
|
+
if (n !== "maintain-framerate") return n;
|
|
21
|
+
let i = Object.values(t.video ?? {}).filter((e) => e !== null);
|
|
22
|
+
return i.length === 0 ? n : Math.max(...i) >= (e.fluidFloorKbps ?? r) ? "maintain-framerate" : "maintain-resolution";
|
|
23
|
+
}
|
|
24
|
+
function o(e, t, n) {
|
|
25
|
+
let r = e.getParameters();
|
|
26
|
+
if (t !== void 0 && (r.degradationPreference = t), n !== void 0) for (let e of r.encodings ?? []) e.maxFramerate = n;
|
|
27
|
+
return r;
|
|
28
|
+
}
|
|
29
|
+
async function s(t, n, r, i) {
|
|
30
|
+
for (let [a, s] of n.entries()) {
|
|
31
|
+
let n = t[a]?.sender;
|
|
32
|
+
if (!n) continue;
|
|
33
|
+
if (s.kind === "audio") {
|
|
34
|
+
let t = r.audio?.[s.name];
|
|
35
|
+
t !== void 0 && await e(n, t);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
let c = r.video?.[s.name];
|
|
39
|
+
if (c !== void 0 && await e(n, c === null ? null : c * 1e3), i !== void 0 || r.maxFramerate !== void 0) try {
|
|
40
|
+
await n.setParameters(o(n, i, r.maxFramerate));
|
|
41
|
+
} catch {
|
|
42
|
+
try {
|
|
43
|
+
await n.setParameters(o(n, void 0, r.maxFramerate));
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { s as applyQualityToLink, a as resolveDegradation, i as scaleForRoom };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=mesh-quality.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mesh-quality.js","names":[],"sources":["../../src/webrtc/mesh-quality.ts"],"sourcesContent":["import { setSenderBitrate } from \"./sender-bitrate\";\nimport type { MeshQuality, MeshSlot } from \"./mesh-types\";\n\n/** Uplink assumed available when the caller names no budget, in kbps. */\nconst DEFAULT_UPLINK_BUDGET_KBPS = 6000;\n\n/** Floor a video slot keeps after the division, in kbps. */\nconst DEFAULT_MIN_VIDEO_KBPS = 300;\n\n/** Budget below which `maintain-framerate` stops being worth holding, in kbps. */\nconst DEFAULT_FLUID_FLOOR_KBPS = 900;\n\n/**\n * Divide the video caps by the size of the room.\n *\n * A mesh sends one copy of everything per participant, so the uplink is the\n * shared resource and the caps are what compete for it. Nothing is divided while\n * the caller is alone with one peer — that is the case the largest sizes exist\n * for, and dividing a budget that is not being shared would make them\n * unreachable in the only call where they fit.\n *\n * The floor is what keeps the division honest: without it a busy room allocates\n * tens of kbps per stream, and everybody loses the picture instead of the excess\n * giving way.\n *\n * @param quality - The caps as asked for.\n * @param peers - How many links are live.\n * @returns The caps to actually apply. The input is never mutated, so a room\n * that empties out climbs back to what was asked for.\n */\nexport function scaleForRoom(quality: MeshQuality, peers: number): MeshQuality {\n const video = quality.video ?? {};\n if (peers <= 1) return quality;\n\n const asked = Object.values(video).reduce<number>((sum, cap) => sum + (cap ?? 0), 0);\n if (asked === 0) return quality;\n\n const perPeer = (quality.uplinkBudgetKbps ?? DEFAULT_UPLINK_BUDGET_KBPS) / peers;\n if (asked <= perPeer) return quality;\n\n const floor = quality.minVideoKbps ?? DEFAULT_MIN_VIDEO_KBPS;\n const factor = perPeer / asked;\n const scaled: Record<string, number | null> = {};\n for (const [slot, cap] of Object.entries(video)) {\n scaled[slot] = cap === null ? null : Math.max(floor, Math.round(cap * factor));\n }\n return { ...quality, video: scaled };\n}\n\n/**\n * Decide what the encoder gives up, letting physics override the preference.\n *\n * `maintain-framerate` is honoured while there are bits enough for the frames to\n * be worth keeping. Once the room's division has taken the budget below the\n * fluid floor, holding the rate halves what each frame receives and the picture\n * is worse than the lower rate it replaced — so below that line the preference\n * is overridden rather than obeyed.\n *\n * @param quality - What was asked for.\n * @param effective - What the room's share actually allows.\n * @returns The degradation preference to write onto the senders, or `undefined`\n * when the caller expressed no preference.\n */\nexport function resolveDegradation(\n quality: MeshQuality,\n effective: MeshQuality,\n): RTCDegradationPreference | undefined {\n const asked = quality.degradationPreference;\n if (asked !== \"maintain-framerate\") return asked;\n\n const caps = Object.values(effective.video ?? {}).filter((cap): cap is number => cap !== null);\n if (caps.length === 0) return asked;\n\n const budget = Math.max(...caps);\n return budget >= (quality.fluidFloorKbps ?? DEFAULT_FLUID_FLOOR_KBPS)\n ? \"maintain-framerate\"\n : \"maintain-resolution\";\n}\n\n/**\n * Read a video sender's parameters back with the motion settings written on.\n *\n * Read fresh on every call because `setParameters` only accepts the object the\n * **same** sender's `getParameters` returned, so a rejected attempt cannot be\n * retried with the object that was rejected.\n *\n * @param sender - The video sender to read from.\n * @param degradation - What to give up first, or `undefined` to leave it alone.\n * @param fps - Frame ceiling, or `undefined` to leave it alone.\n * @returns Parameters ready to be written back to `sender`.\n */\nfunction videoParameters(\n sender: RTCRtpSender,\n degradation: RTCDegradationPreference | undefined,\n fps: number | undefined,\n): RTCRtpSendParameters {\n const params = sender.getParameters();\n if (degradation !== undefined) params.degradationPreference = degradation;\n if (fps !== undefined) {\n for (const encoding of params.encodings ?? []) encoding.maxFramerate = fps;\n }\n return params;\n}\n\n/**\n * Write the quality settings onto one link's senders.\n *\n * `degradationPreference` goes only on video: it describes trading resolution\n * against frame rate, which an audio sender has no analogue for, and some\n * browsers reject it outright there.\n *\n * The retry without `degradationPreference` is for Firefox, which rejects the\n * member entirely — sending both together would lose the frame-rate cap to an\n * objection about something else.\n *\n * @param transceivers - The link's transceivers, in slot order.\n * @param slots - The slot list those transceivers were allocated from.\n * @param effective - Caps after the room's division.\n * @param degradation - Already resolved against the fluid floor.\n */\nexport async function applyQualityToLink(\n transceivers: readonly RTCRtpTransceiver[],\n slots: readonly MeshSlot[],\n effective: MeshQuality,\n degradation: RTCDegradationPreference | undefined,\n): Promise<void> {\n for (const [index, slot] of slots.entries()) {\n const sender = transceivers[index]?.sender;\n if (!sender) continue;\n\n if (slot.kind === \"audio\") {\n const bps = effective.audio?.[slot.name];\n if (bps !== undefined) await setSenderBitrate(sender, bps);\n continue;\n }\n\n const kbps = effective.video?.[slot.name];\n if (kbps !== undefined) await setSenderBitrate(sender, kbps === null ? null : kbps * 1000);\n\n if (degradation === undefined && effective.maxFramerate === undefined) continue;\n try {\n await sender.setParameters(\n videoParameters(sender, degradation, effective.maxFramerate),\n );\n } catch {\n try {\n await sender.setParameters(\n videoParameters(sender, undefined, effective.maxFramerate),\n );\n } catch {\n /* the sender refused both; the bitrate cap above still applies */\n }\n }\n }\n}\n"],"mappings":";;AAIA,IAAM,IAA6B,KAG7B,IAAyB,KAGzB,IAA2B;AAoBjC,SAAgB,EAAa,GAAsB,GAA4B;CAC3E,IAAM,IAAQ,EAAQ,SAAS,CAAC;CAChC,IAAI,KAAS,GAAG,OAAO;CAEvB,IAAM,IAAQ,OAAO,OAAO,CAAK,CAAC,CAAC,QAAgB,GAAK,MAAQ,KAAO,KAAO,IAAI,CAAC;CACnF,IAAI,MAAU,GAAG,OAAO;CAExB,IAAM,KAAW,EAAQ,oBAAoB,KAA8B;CAC3E,IAAI,KAAS,GAAS,OAAO;CAE7B,IAAM,IAAQ,EAAQ,gBAAgB,GAChC,IAAS,IAAU,GACnB,IAAwC,CAAC;CAC/C,KAAK,IAAM,CAAC,GAAM,MAAQ,OAAO,QAAQ,CAAK,GAC1C,EAAO,KAAQ,MAAQ,OAAO,OAAO,KAAK,IAAI,GAAO,KAAK,MAAM,IAAM,CAAM,CAAC;CAEjF,OAAO;EAAE,GAAG;EAAS,OAAO;CAAO;AACvC;AAgBA,SAAgB,EACZ,GACA,GACoC;CACpC,IAAM,IAAQ,EAAQ;CACtB,IAAI,MAAU,sBAAsB,OAAO;CAE3C,IAAM,IAAO,OAAO,OAAO,EAAU,SAAS,CAAC,CAAC,CAAC,CAAC,QAAQ,MAAuB,MAAQ,IAAI;CAI7F,OAHI,EAAK,WAAW,IAAU,IAEf,KAAK,IAAI,GAAG,CACpB,MAAW,EAAQ,kBAAkB,KACtC,uBACA;AACV;AAcA,SAAS,EACL,GACA,GACA,GACoB;CACpB,IAAM,IAAS,EAAO,cAAc;CAEpC,IADI,MAAgB,KAAA,MAAW,EAAO,wBAAwB,IAC1D,MAAQ,KAAA,GACR,KAAK,IAAM,KAAY,EAAO,aAAa,CAAC,GAAG,EAAS,eAAe;CAE3E,OAAO;AACX;AAkBA,eAAsB,EAClB,GACA,GACA,GACA,GACa;CACb,KAAK,IAAM,CAAC,GAAO,MAAS,EAAM,QAAQ,GAAG;EACzC,IAAM,IAAS,EAAa,EAAM,EAAE;EACpC,IAAI,CAAC,GAAQ;EAEb,IAAI,EAAK,SAAS,SAAS;GACvB,IAAM,IAAM,EAAU,QAAQ,EAAK;GACnC,AAAI,MAAQ,KAAA,KAAW,MAAM,EAAiB,GAAQ,CAAG;GACzD;EACJ;EAEA,IAAM,IAAO,EAAU,QAAQ,EAAK;EACpC,IAAI,MAAS,KAAA,KAAW,MAAM,EAAiB,GAAQ,MAAS,OAAO,OAAO,IAAO,GAAI,GAErF,MAAgB,KAAA,KAAa,EAAU,iBAAiB,KAAA,GAC5D,IAAI;GACA,MAAM,EAAO,cACT,EAAgB,GAAQ,GAAa,EAAU,YAAY,CAC/D;EACJ,QAAQ;GACJ,IAAI;IACA,MAAM,EAAO,cACT,EAAgB,GAAQ,KAAA,GAAW,EAAU,YAAY,CAC7D;GACJ,QAAQ,CAER;EACJ;CACJ;AACJ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e){return new RTCPeerConnection({iceServers:e,bundlePolicy:`max-bundle`,rtcpMuxPolicy:`require`})}function t(e,t,n){let r=Number(e.mid);if(e.mid!==null&&Number.isInteger(r)&&n[r])return n[r];let i=t.transceivers.indexOf(e);return i>=0?n[i]??null:null}async function n(e){let t=e.pendingCandidates.splice(0,e.pendingCandidates.length);for(let n of t)try{await e.pc.addIceCandidate(n)}catch{}}async function r(e,t){if(!e.remoteReady){e.pendingCandidates.push(t);return}try{await e.pc.addIceCandidate(t)}catch{}}async function i(e,t,n){for(let[r,i]of t.entries()){let t=e.transceivers[r],a=n.get(i.name)??null;if(!(!t||!a))try{await t.sender.replaceTrack(a)}catch{}}}function a(e){e.transceivers=e.pc.getTransceivers().filter(e=>e.mid!==null).sort((e,t)=>Number(e.mid)-Number(t.mid));for(let t of e.transceivers)t.direction!==`sendrecv`&&(t.direction=`sendrecv`)}exports.acceptCandidate=r,exports.adoptTransceivers=a,exports.attachLocalTracks=i,exports.createPeerConnection=e,exports.drainCandidates=n,exports.slotOf=t;
|
|
2
|
+
//# sourceMappingURL=peer-link.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peer-link.cjs","names":[],"sources":["../../src/webrtc/peer-link.ts"],"sourcesContent":["import type { MeshSlot } from \"./mesh-types\";\n\n/** One remote peer's connection and everything the mesh tracks about it. */\nexport interface PeerLink {\n peerId: string;\n pc: RTCPeerConnection;\n /**\n * Transceivers in slot order.\n *\n * Held explicitly rather than re-read from `getTransceivers()` so slot\n * lookup stays O(1) and cannot drift if a browser reorders.\n */\n transceivers: RTCRtpTransceiver[];\n /** Candidates that arrived before the remote description they belong to. */\n pendingCandidates: RTCIceCandidateInit[];\n /** Whether `setRemoteDescription` has landed, which gates the queue. */\n remoteReady: boolean;\n /** Whether an offer is in flight, so a re-entrant call does not stack one. */\n makingOffer: boolean;\n /** Whether this side offers. Exactly one side of a link does. */\n isOfferer: boolean;\n}\n\n/** Open a connection with the policies a mesh needs. */\nexport function createPeerConnection(iceServers: RTCIceServer[]): RTCPeerConnection {\n return new RTCPeerConnection({\n iceServers,\n bundlePolicy: \"max-bundle\",\n rtcpMuxPolicy: \"require\",\n });\n}\n\n/**\n * Resolve which slot a transceiver carries.\n *\n * The offerer allocates the m-lines in slot order, so a transceiver's `mid` is\n * its ordinal and both ends read the same one. That makes `mid` the slot's wire\n * identity, and the **only** identifier correct on both sides of a link.\n *\n * Position in the link's own array is a fallback for a `mid` that is not a plain\n * ordinal, and never the primary: a browser answering an offer appends the\n * negotiated transceivers *after* any it already held, so positional lookup\n * silently resolves to the wrong slot — the camera arriving in the screen tile —\n * or to none at all, dropping the track.\n *\n * @param transceiver - The transceiver a track arrived on.\n * @param link - The link it belongs to.\n * @param slots - The mesh's slot list.\n * @returns The slot, or `null` when neither route names one.\n */\nexport function slotOf(\n transceiver: RTCRtpTransceiver,\n link: PeerLink,\n slots: readonly MeshSlot[],\n): MeshSlot | null {\n const ordinal = Number(transceiver.mid);\n if (transceiver.mid !== null && Number.isInteger(ordinal) && slots[ordinal]) {\n return slots[ordinal];\n }\n const index = link.transceivers.indexOf(transceiver);\n return index >= 0 ? (slots[index] ?? null) : null;\n}\n\n/**\n * Feed a link every candidate that arrived before its remote description.\n *\n * `addIceCandidate` throws while `remoteDescription` is `null`, and candidates\n * routinely beat the offer or answer they belong to. Without the queue the\n * connection loses them and sits in `checking` until it times out — a failure\n * with no error anywhere.\n *\n * @param link - The link whose queue is drained.\n */\nexport async function drainCandidates(link: PeerLink): Promise<void> {\n const queued = link.pendingCandidates.splice(0, link.pendingCandidates.length);\n for (const candidate of queued) {\n try {\n await link.pc.addIceCandidate(candidate);\n } catch {\n /* a candidate the browser rejects is not fatal to the connection */\n }\n }\n}\n\n/**\n * Hand a candidate to the connection, or park it until the description lands.\n *\n * @param link - The link the candidate belongs to.\n * @param init - The candidate, already in `RTCIceCandidateInit` shape.\n */\nexport async function acceptCandidate(link: PeerLink, init: RTCIceCandidateInit): Promise<void> {\n if (!link.remoteReady) {\n link.pendingCandidates.push(init);\n return;\n }\n try {\n await link.pc.addIceCandidate(init);\n } catch {\n /* a candidate the browser rejects is not fatal to the connection */\n }\n}\n\n/**\n * Publish every held local track onto a link's slots.\n *\n * Called once the transceivers exist — up front for the offerer, after the offer\n * lands for the answerer — so a microphone that was already live when the peer\n * joined starts flowing without waiting for a toggle.\n *\n * @param link - The link to publish onto.\n * @param slots - The mesh's slot list.\n * @param tracks - The current local track per slot name.\n */\nexport async function attachLocalTracks(\n link: PeerLink,\n slots: readonly MeshSlot[],\n tracks: Map<string, MediaStreamTrack | null>,\n): Promise<void> {\n for (const [index, slot] of slots.entries()) {\n const transceiver = link.transceivers[index];\n const track = tracks.get(slot.name) ?? null;\n if (!transceiver || !track) continue;\n try {\n await transceiver.sender.replaceTrack(track);\n } catch {\n /* the browser refused the track; the slot simply stays silent */\n }\n }\n}\n\n/**\n * Adopt the transceivers a remote offer created, in `mid` order.\n *\n * The answering side allocates **nothing** up front, and that is the whole\n * subtlety: applying a remote offer creates one transceiver per m-line, in\n * m-line order, and browsers do not reuse transceivers the answerer made\n * beforehand. Pre-allocating leaves dead, never-negotiated transceivers sitting\n * in front of the live ones, which is how slot lookup ends up pointing at the\n * wrong media or at nothing.\n *\n * Each adopted transceiver is switched to `sendrecv` so this side may publish on\n * it too; without that a peer that answered could hear but never be heard.\n *\n * @param link - The link that just applied a remote offer.\n */\nexport function adoptTransceivers(link: PeerLink): void {\n link.transceivers = link.pc\n .getTransceivers()\n .filter((transceiver) => transceiver.mid !== null)\n .sort((a, b) => Number(a.mid) - Number(b.mid));\n for (const transceiver of link.transceivers) {\n if (transceiver.direction !== \"sendrecv\") transceiver.direction = \"sendrecv\";\n }\n}\n"],"mappings":"AAwBA,SAAgB,EAAqB,EAA+C,CAChF,OAAO,IAAI,kBAAkB,CACzB,aACA,aAAc,aACd,cAAe,SACnB,CAAC,CACL,CAoBA,SAAgB,EACZ,EACA,EACA,EACe,CACf,IAAM,EAAU,OAAO,EAAY,GAAG,EACtC,GAAI,EAAY,MAAQ,MAAQ,OAAO,UAAU,CAAO,GAAK,EAAM,GAC/D,OAAO,EAAM,GAEjB,IAAM,EAAQ,EAAK,aAAa,QAAQ,CAAW,EACnD,OAAO,GAAS,EAAK,EAAM,IAAU,KAAQ,IACjD,CAYA,eAAsB,EAAgB,EAA+B,CACjE,IAAM,EAAS,EAAK,kBAAkB,OAAO,EAAG,EAAK,kBAAkB,MAAM,EAC7E,IAAK,IAAM,KAAa,EACpB,GAAI,CACA,MAAM,EAAK,GAAG,gBAAgB,CAAS,CAC3C,MAAQ,CAER,CAER,CAQA,eAAsB,EAAgB,EAAgB,EAA0C,CAC5F,GAAI,CAAC,EAAK,YAAa,CACnB,EAAK,kBAAkB,KAAK,CAAI,EAChC,MACJ,CACA,GAAI,CACA,MAAM,EAAK,GAAG,gBAAgB,CAAI,CACtC,MAAQ,CAER,CACJ,CAaA,eAAsB,EAClB,EACA,EACA,EACa,CACb,IAAK,GAAM,CAAC,EAAO,KAAS,EAAM,QAAQ,EAAG,CACzC,IAAM,EAAc,EAAK,aAAa,GAChC,EAAQ,EAAO,IAAI,EAAK,IAAI,GAAK,KACnC,MAAC,GAAe,CAAC,GACrB,GAAI,CACA,MAAM,EAAY,OAAO,aAAa,CAAK,CAC/C,MAAQ,CAER,CACJ,CACJ,CAiBA,SAAgB,EAAkB,EAAsB,CACpD,EAAK,aAAe,EAAK,GACpB,gBAAgB,CAAC,CACjB,OAAQ,GAAgB,EAAY,MAAQ,IAAI,CAAC,CACjD,MAAM,EAAG,IAAM,OAAO,EAAE,GAAG,EAAI,OAAO,EAAE,GAAG,CAAC,EACjD,IAAK,IAAM,KAAe,EAAK,aACvB,EAAY,YAAc,aAAY,EAAY,UAAY,WAE1E"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/webrtc/peer-link.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
return new RTCPeerConnection({
|
|
4
|
+
iceServers: e,
|
|
5
|
+
bundlePolicy: "max-bundle",
|
|
6
|
+
rtcpMuxPolicy: "require"
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
function t(e, t, n) {
|
|
10
|
+
let r = Number(e.mid);
|
|
11
|
+
if (e.mid !== null && Number.isInteger(r) && n[r]) return n[r];
|
|
12
|
+
let i = t.transceivers.indexOf(e);
|
|
13
|
+
return i >= 0 ? n[i] ?? null : null;
|
|
14
|
+
}
|
|
15
|
+
async function n(e) {
|
|
16
|
+
let t = e.pendingCandidates.splice(0, e.pendingCandidates.length);
|
|
17
|
+
for (let n of t) try {
|
|
18
|
+
await e.pc.addIceCandidate(n);
|
|
19
|
+
} catch {}
|
|
20
|
+
}
|
|
21
|
+
async function r(e, t) {
|
|
22
|
+
if (!e.remoteReady) {
|
|
23
|
+
e.pendingCandidates.push(t);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
await e.pc.addIceCandidate(t);
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
async function i(e, t, n) {
|
|
31
|
+
for (let [r, i] of t.entries()) {
|
|
32
|
+
let t = e.transceivers[r], a = n.get(i.name) ?? null;
|
|
33
|
+
if (!(!t || !a)) try {
|
|
34
|
+
await t.sender.replaceTrack(a);
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function a(e) {
|
|
39
|
+
e.transceivers = e.pc.getTransceivers().filter((e) => e.mid !== null).sort((e, t) => Number(e.mid) - Number(t.mid));
|
|
40
|
+
for (let t of e.transceivers) t.direction !== "sendrecv" && (t.direction = "sendrecv");
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { r as acceptCandidate, a as adoptTransceivers, i as attachLocalTracks, e as createPeerConnection, n as drainCandidates, t as slotOf };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=peer-link.js.map
|