Files
suaveplan/openspec/archive/2026-05-23-enterprise-react-audio-stream-hooks-components/proposal.md
2026-05-27 23:29:53 +01:00

3.5 KiB
Raw Blame History

@suaveplan/react-audio-stream — hooks + components (chunk C)

  • Target path: packages/ui/react/react-audio-stream/
  • Tier: browser/react
  • Date: 2026-05-23
  • Status: PROPOSED — chunk C of the 4-chunk enterprise-react-audio-stream split.

Why

Chunks A (session-core) and B (transports) deliver the headless engine — the FSM, transports, source-plugin bridge, error codes, telemetry. This chunk delivers the React surface that consumers actually touch: a Provider, 6 hooks, 4 accessible components. Transport-share concerns (the 7th hook and its component indicator) live in chunk D so this chunk stays disjoint.

What changes

  1. <StreamProvider> — src/provider/stream-provider.tsx wires a StreamSession instance into React context. sessionFactory injectable (consumer creates the session with their chosen transport adapter); StrictMode-safe singleton; surfaces 'state-changed'/'interruption' events into hook subscribers via a typed event bus.
  2. Hooks (each in its own feature dir with .ts + .types.ts + .test.tsx + .md + index.ts):
    • useStreamSession() — returns the session + current FSM state + lifecycle methods (connect, join, leave, disconnect)
    • useStreamRoom() — returns { roomId, isJoined, joinedAt, participants } derived from session state
    • useStreamPeers() — returns live Peer[] (id + display name + connection state + RTT)
    • useStreamMixer() — exposes per-peer setVolume, setMute, setSolo (DAW solo semantics — when any peer is soloed, every non-soloed peer's track-volume → 0); reads from each peer chain's setTrackVolume/setTrackMute/setTrackSolo
    • useStreamLevels(peerId?) — returns realtime peak/RMS for the local mic or the named peer; piggybacks on @suaveplan/audio-analysis-spectrum's level computation via the peer chain's analyzer slot
    • useStreamRecording() — start(), stop() returning a Blob; mixes the active session output (or a subset of peers) into a single audio file via audio-engine-browser's offline render
    • useStreamPermissions() — wraps navigator.mediaDevices.getUserMedia permission state with the typed MediaPermissionState enum
  3. Components (each with .tsx + .types.ts + .test.tsx + .stories.tsx + .md + index.ts):
    • <StreamRoom> — controlled wrapper exposing room state + join/leave controls; render-prop pattern for layout flexibility
    • <StreamMixer> — per-peer mixer strip with level meter + mute/solo buttons + volume slider; delegates to useStreamMixer()
    • <StreamRecorder> — record/stop button with duration display; delegates to useStreamRecording()
    • <StreamPermissionsBanner> — permission state banner with explicit request button; ARIA-live for state changes
  4. Subpath exports — package.json adds ./provider, ./hooks, ./hooks/use-stream-session (and 5 more), ./components, ./components/stream-room (and 3 more).

Requirements landed by this chunk

From the original spec:

  • Per-peer stream mixer (full — <StreamMixer> + useStreamMixer)
  • Subpath exports for ./provider, ./hooks/*, ./components/*

Impact

  • ~35 files (6 hooks × 5 + 4 components × 6 + provider + barrel touches).
  • Imports StreamSession, StreamSourcePlugin, ReactAudioStreamError from chunk A — local-mirror types in tests if A hasn't merged.
  • Imports nothing from chunk B (consumers wire transports themselves via the session factory).
  • No spec fold / no archive — chunk D performs both.