# @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...


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

```js
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.
- `getUserMedia` **never 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.
- `useMicLevel` measures **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`.


---
Source: shared/packages/devices/README.md
Canonical: https://docs.leumas.tech/p/packages/devices
