feat(calls): select mic/camera/speaker instead of system default (LC-144) #172

Merged
nrupard merged 2 commits from feat/lc-144-call-device-select into main 2026-05-21 16:40:09 +02:00
Owner

What

LC-144: let a user pick which microphone, camera, and speaker a call/voice session uses instead of always taking the system default. Applies to both 1:1 calls and enclave voice channels; selections persist across reloads.

How

  • New shared module server/assets/devices.js (window.LetsChatDevices) owns the preference (localStorage, keyed by MediaDeviceKind), builds the getUserMedia constraints, routes the speaker via setSinkId, and renders the picker modal opened from any [data-lc-open-devices] control.
  • call.js and voice.js acquire all media through the module: getUserMedia honors the pinned mic/camera as an exact deviceId, falling back to defaults when the device is gone (OverconstrainedError/NotFoundError). Camera-only paths (camera toggle, screen-share restore) honor the pinned camera too.
  • Remote audio is routed to the pinned speaker on track attach and re-routed live mid-call when the speaker changes (lc:speaker-change custom event). Speaker row hidden where setSinkId is unsupported (Firefox).
  • Picker enumerates devices, refreshes on the devicechange event while open, and offers a transient "Show device names" grant when labels are blank (no permission yet).
  • Gear control added to the DM header, the active-call control bar, and the voice-channel header.
  • Tailwind content now scans assets/**/*.js (excluding vendor/) so JS-injected utility classes (the picker, and the existing voice tile markup) are generated.

Scope notes (v1)

  • Mid-call input switching via replaceTrack is deferred (issue stretch goal): mic/camera apply at the next call start; only the speaker re-routes live.
  • Persistence is global client-side, not per room/enclave, and not server-stored. Matches the issue's proposed approach.
  • Pre-call device test (mic meter / camera preview) not included.

Test

  • No Rust changed (frontend JS + templates + tailwind config). just check clean (standalone + saas, clippy + fmt). just test and just test-saas both green (0 failures). Askama templates compile with the new markup.
## What LC-144: let a user pick which microphone, camera, and speaker a call/voice session uses instead of always taking the system default. Applies to both 1:1 calls and enclave voice channels; selections persist across reloads. ## How - New shared module `server/assets/devices.js` (`window.LetsChatDevices`) owns the preference (localStorage, keyed by `MediaDeviceKind`), builds the getUserMedia constraints, routes the speaker via `setSinkId`, and renders the picker modal opened from any `[data-lc-open-devices]` control. - `call.js` and `voice.js` acquire all media through the module: `getUserMedia` honors the pinned mic/camera as an `exact` deviceId, falling back to defaults when the device is gone (OverconstrainedError/NotFoundError). Camera-only paths (camera toggle, screen-share restore) honor the pinned camera too. - Remote audio is routed to the pinned speaker on track attach and re-routed live mid-call when the speaker changes (`lc:speaker-change` custom event). Speaker row hidden where `setSinkId` is unsupported (Firefox). - Picker enumerates devices, refreshes on the `devicechange` event while open, and offers a transient "Show device names" grant when labels are blank (no permission yet). - Gear control added to the DM header, the active-call control bar, and the voice-channel header. - Tailwind `content` now scans `assets/**/*.js` (excluding `vendor/`) so JS-injected utility classes (the picker, and the existing voice tile markup) are generated. ## Scope notes (v1) - Mid-call input switching via `replaceTrack` is deferred (issue stretch goal): mic/camera apply at the next call start; only the speaker re-routes live. - Persistence is global client-side, not per room/enclave, and not server-stored. Matches the issue's proposed approach. - Pre-call device test (mic meter / camera preview) not included. ## Test - No Rust changed (frontend JS + templates + tailwind config). `just check` clean (standalone + saas, clippy + fmt). `just test` and `just test-saas` both green (0 failures). Askama templates compile with the new markup.
feat(calls): select mic/camera/speaker instead of system default (LC-144)
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 1m31s
1dca2f13ce
Adds a shared device-selection module (assets/devices.js) used by both 1:1 calls (call.js) and enclave voice channels (voice.js). A gear control in the DM header, the active-call control bar, and the voice-channel header opens a picker that enumerates microphones, cameras, and speakers and persists the choice in localStorage keyed by device kind.

Acquisition now routes through the module: getUserMedia honors the pinned mic/camera as an exact deviceId constraint, falling back to the system default if the pinned device has been unplugged (OverconstrainedError/NotFoundError). Camera-only acquisition (camera toggle, screen-share restore) honors the pinned camera too. Remote audio is routed to the pinned speaker via setSinkId on attach, and re-routed live when the speaker is changed mid-call (lc:speaker-change event). The speaker row is hidden where setSinkId is unsupported (Firefox), and the picker refreshes its lists on the devicechange event while open. Selections persist across reloads and apply to both call types.

Device labels require a getUserMedia grant; before any call the picker shows generic labels with a "Show device names" button that takes a transient grant and re-enumerates.

Tailwind content globs now include assets/**/*.js (excluding vendor) so utility classes injected from JS, including the picker markup, are generated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(calls): guard device picker against stale refresh + idle speaker re-route (LC-144)
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 1m22s
Create release / Create release from merged PR (pull_request) Has been skipped
eff79289cd
Two review fixes. refresh() now bails if the picker was closed while enumerateDevices() was still pending, so a late-resolving enumeration no longer rebuilds a detached modal. The lc:speaker-change listener in call.js skips applySpeaker when no call is active (phase === 'idle'), keeping the speaker re-route scoped to live calls.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-144-call-device-select 2026-05-21 16:40:09 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!172
No description provided.