@leumas/devices
The shared device layer. Today: the browser half — enumerating media devices, the ONE permission latch a denied prompt sets, camera streams that are ref-counted and time-out on a hung prompt, a mic...
@leumas/devices
The shared device layer behind Studio's Devices HUD (/admin/devices).
Today it ships the browser half — everything a surface needs to list, open, test and capture the cameras, microphones and speakers attached to the machine the browser is running on. Nothing in it talks to an API.
Why it exists
Three copies of "enumerate the media devices" were already in the tree, each grouping, labelling and failing slightly differently, so the same machine described itself differently depending on which surface you opened:
| Was | Now |
|---|---|
products/leumas-studio/src/admin/surveillance/utils/browserCamera.js | a re-export shim over ./media/camera.js |
shared/packages/os/src/apps/device-manager/DeviceManager.jsx | consumes useMediaDevices() |
shared/packages/splats/src/capture/recorder.js | generalised into ./media/recorder.js |
@leumas/sing's createMicSource keeps its own latch: it is bound to Sing's AudioContext lifecycle and its game loop, and prising that apart would be a refactor of a working game for no visible gain. What is shared here is the rule, not the plumbing.
The one rule
A refused permission LATCHES OFF. A denied getUserMedia can never be fixed by asking again — the browser remembers the decision — so a surface that re-requests on the next render prompts forever. permission.js holds a sticky per-kind denied state that only retryMedia(kind) clears, and it must be called from a real click. This has shipped as a live defect in this repo before.
API
import {
useMediaDevices, // cameras/microphones/speakers + `devicechange` + needsPermission
useMicLevel, // RMS meter for one mic; tears down track, node and AudioContext
requestMedia, retryMedia, stopStream, describeMediaError, // the latch
listCameraDevices, startBrowserCamera, stopBrowserCamera, captureFrame,
isRecordingSupported, startRecording, downloadBlob, stamp,
} from '@leumas/devices/media';
import { TRANSPORTS, SIDES, STATES, isPresent } from '@leumas/devices';
The barrel exports nothing from ./media on purpose: that half is React and browser-only, and a Node consumer (a guard, the API, a test) importing the barrel must not be handed navigator.
Traps worth knowing
- Before any grant,
enumerateDevices()returns the right number of devices with **empty labels
and empty ids**. Nothing is broken — the browser is refusing to fingerprint you. needsPermission is the tell; ensureLabels() asks once, through the latch, and stops the probe stream immediately.
getUserMedianever settles while a prompt sits unanswered, so caching the pending promise (to
share one camera between tiles) makes a hung prompt immortal. camera.js races it against a 15s timeout for that reason alone.
- A preview holds the camera open. Stop every track on unmount or the recording light stays on and
the next app to ask is told the device is busy.
setSinkId(routing audio to a chosen output) is Chromium-only. Show it disabled with a
reason rather than hiding it.
useMicLevelmeasures RMS, not peak: a peak meter pins to 1.0 on the first consonant and says
"working" for a mic that has picked up one click and gone silent.
Next
Pass 4 of the Devices roadmap adds the isomorphic half to the barrel — the device record, defineDeviceDriver, defineDeviceProvider — plus a ./host subpath for the machine-local reads (ports, adapters, the paired Bluetooth list) that only mount under caps.localHardware.
What it exports
166 exports: 35 components, 2 hooks, 129 helpers. Import from @leumas/devices.
AGGREGATESAUDIO_SERVICESBAUD_RATESCAPABILITY_RECHANNEL_TYPESCHANNELS_PER_MUXCODE_ALIASESDEFAULT_DEADLINE_MSDEFAULT_ENTRY_LIMITDEFAULT_LIMITSDEFAULT_MAX_DEPTHDRIVE_KINDSEEPROM_WRITINGEMPTY_CODEEMPTY_LAYOUTFALLBACK_MSIDLE_MSIS_MACIS_WINMAX_LINE_BYTESMQTT_INSTALL_HINTMUX_COUNTOFFLINE_MSOUTPUTSOVERFLOW_MODESPROBE_PORTSPROBE_TIMEOUT_MSRAW_MAXRAW_MINRULE_SAFESEMANTIC_TYPESSIDESSSDP_AUDIO_TARGETSSTATESTRANSPORTSuseMediaDevicesuseMicLeveladapterThroughputapplyCalibrationarpNeighboursbleDriverbuildSizeTreecapabilitiesFromLayoutcapabilityIdcaptureFramecaptureFrameToBlobchannelsOfconvertTemperaturecreateDeviceRegistrycreateGroboticsSessioncreateGroboticsWsListenercreateLineSplittercreateRecorderdecodeFramedecodeLinedecodeReadingdefineDeviceDriverdefineDeviceProviderdescribeBrowserCameraErrordescribeBrowserMicErrordescribeMediaErrordescribeScreenShareErrordiffLayoutsdiscoverdownloadBlobencodeQueryfakeClockfromLegacySetupgetBrowserDrivergetConfiggetMediaErrorgetMediaStategoodbyeInstancesgroboticsSerialDriverhasWebBluetoothhasWebHidhasWebSerialisChromiumisPresentisPrivateIpv4isRandomizedMacisRecordingSupportedisScreenShareSupportedisSecurekillPortlabelDriftlayoutHashlayoutWarningslistAdapterslistAudioInputsAsynclistAudioOutputslistAudioOutputsAsynclistBrowserDriverslistCameraDeviceslistMicDeviceslistPortslistVolumeslocalSubnetsmapRawToPercentmdnsListenmdnsProbemockTransportmqttDrivernetworkSnapshotnormalizeDevicenormalizeLayoutnormalizeLayoutsnormalizeReadingonMediaStateouiVendorpairedBluetoothpairedBluetoothAsyncparseAnswersparseCapabilityIdparsePactlpauseStreampidsOnPortpingplatformWarningspresenceOfpreviouslyPermittedBluetoothpsJsonrawToMilliCelsiusreadCharacteristicreadNameregisterBrowserDriverrequestMediaretryMediarouteInforunsanitizeDevicesaveThresholdsscaleReadingscansendIrserialDriverserialTransportsetConfigsetOutputsetThresholdshapeBluetoothshapeWindowsshouldIgnorespawnAsyncssdpListenssdpProbestampstartBrowserCamerastartBrowserMicstartRecordingstartScreenSharestopBrowserCamerastopBrowserMicstopScreenSharestopStreamstreamsupportsCanvasFiltertopicMatchestypeForCodetypeForSemantictypeForWireunavailableReasonwatchAudioDeviceswindowsLabelwirelessStatuswritesEeprom