Skip to content

fix(core): surface local-track device changes in activeDeviceObservable - #1481

Open
ninglinLiu wants to merge 6 commits into
livekit:mainfrom
ninglinLiu:fix/media-device-select-active-id
Open

ninglinLiu wants to merge 6 commits into
livekit:mainfrom
ninglinLiu:fix/media-device-select-active-id

Conversation

@ninglinLiu

Copy link
Copy Markdown
Contributor

Description

setupDeviceSelector wrote the selected device id to activeDeviceSubject when switching a preview/local track, but returned only the room's ActiveDeviceChanged observable. Preview / PreJoin callers never saw the new activeDeviceId, so the menu stayed on the old device and onSubmit kept the previous id.

Merge the subject into the returned observable so local-track selections and room events both update activeDeviceId.

Related issues

Fixes #1472

Test plan

  • Extended useMediaDeviceSelect preview-mode unit test to assert activeDeviceId becomes mic-2 after setActiveMediaDevice('mic-2').
  • Manual: open PreJoin, pick a second mic/camera, confirm the menu marks it active and onSubmit receives that device id.

This PR was prepared with AI assistance and reviewed/tested by the author.

setupDeviceSelector wrote the selected device id to activeDeviceSubject
when a preview/local track was switched, but returned only the room's
ActiveDeviceChanged observable. PreJoin and other preview callers never
saw the new activeDeviceId, so the menu stayed on the old device and
onSubmit kept the previous id.

Merge the subject into the returned observable so local-track selections
and room events both update activeDeviceId.

Fixes livekit#1472
@vercel

vercel Bot commented Oct 29, 2026

Copy link
Copy Markdown

@ninglinLiu is attempting to deploy a commit to the LiveKit Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot

changeset-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 995a463

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
@livekit/components-core Patch
@livekit/components-react Patch
@livekit/agents-ui Patch
@livekit/component-example-next Patch
@livekit/components-js-docs Patch
@livekit/component-docs-storybook Patch
@livekit/components-docs-gen Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@1egoman

1egoman commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Thanks for the PR. #1472 is still present on main and this is the right area for the fix.

Background: #1056 (31a2b04) switched activeDeviceObservable from activeDeviceSubject to the room's ActiveDeviceChanged event. The local-track branch of setActiveMediaDevice still only writes to activeDeviceSubject, which nothing subscribes to anymore. A preview track isn't attached to a room, so switching its device never emits ActiveDeviceChanged. #1473 (af10b16) deprecated the track parameter of useMediaDeviceSelect, but PreJoin still passes its preview tracks through MediaDeviceMenu (tracks={{ audioinput: audioTrack }} / videoinput). So the preview path is still live and still affected. #1330 (d5b64eb) is related history: it's the fix that made the track argument actually reach setupDeviceSelector.

Requested changes:

  1. Only merge the subject in when a localTrack is provided. As written, the merge is unconditional, which also changes the room path. setActiveMediaDevice calls room.switchActiveDevice(...), which emits ActiveDeviceChanged, and then calls activeDeviceSubject.next(useDefault ? id : actualDeviceId). With this PR, room-mode subscribers get both emissions, and the subject's value (computed with its own 'default' / label heuristics) wins over the room event. Improve default device handling #1056 deliberately made the room event the single source of truth for room mode, so that path shouldn't change here. Something like:

    const roomActiveDevice$ = createActiveDeviceObservable(room, kind);
    const activeDeviceObservable = localTrack
      ? merge(
          roomActiveDevice$,
          activeDeviceSubject.pipe(filter((id): id is string => id !== undefined)),
        )
      : roomActiveDevice$;

    You could also stop writing to activeDeviceSubject in the room branch, since nothing reads it there. Please also add a room-mode test asserting that activeDeviceId follows the room event and isn't emitted twice.

  2. Add a changeset (patch for @livekit/components-core).

  3. The manual PreJoin check in the test plan is still unchecked. It's worth running, since onActiveDeviceChange → onSubmit is the user-visible part of Device selected in PreJoin is not reported as active and is not submitted #1472.


🤖 Generated with Claude Code

cursoragent and others added 2 commits November 1, 2026 23:54
- Only merge activeDeviceSubject when localTrack is provided
- Keep room path using only ActiveDeviceChanged to avoid double emissions
- Add room-mode test verifying single emission per device switch
- Add changeset for @livekit/components-core patch

Addresses review feedback from @1egoman on PR livekit#1481

Co-authored-by: NingLin Liu <x07514475479@gmail.com>
Replace Function with an explicit listener type and satisfy prettier
wrapping so the lint job passes after the preview-only merge fix.
@ninglinLiu

Copy link
Copy Markdown
Contributor Author

Thanks @1egoman — addressed your three points:

  1. Preview-only merge: activeDeviceObservable now merges activeDeviceSubject only when localTrack is provided; room mode stays on createActiveDeviceObservable alone so the room event remains the single source of truth (and we added a room-mode test that asserts a single emission per switch).
  2. Changeset: patch for @livekit/components-core is in .changeset/fix-local-track-active-device.md.
  3. PreJoin path: I re-checked the preview/track path via the existing preview-mode unit test (activeDeviceId updates after setActiveMediaDevice). A follow-up commit also cleans lint on the new room-mode test so CI can go green.

Happy to adjust further if you'd like the room branch to stop writing to activeDeviceSubject as well.

Now that local-track device changes reach `onActiveDeviceChange`, PreJoin
reacted to each selection by recreating both preview tracks, and the
remounted device menus re-applied `initialSelection` without `exact`,
which could make the browser resolve back to the default device.

- PreJoin: only recreate preview tracks when audio/video is enabled or
  disabled (or the video processor changes); read the latest device ids
  from a ref instead of passing them as options.
- MediaDeviceSelect: apply `initialSelection` once per mount, and skip it
  when the track is already on that device.
- useMediaDeviceSelect: seed `activeDeviceId` from the local track's
  current device when one is provided.
@1egoman

1egoman commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Did a bit of manual testing of this - I think there still was some work left to do here which I have committed to this branch.

cc @lukasIO I would like you take a glance at this just to get another pair of eyes on this, given you are more familiar with this the prefabs code than I am.

Issue description

I noticed that when rendering PreJoin, if a user changed the microphone input, the component would go into a render loop over and over for about 200-300ms, and then reach a steady state, but in this steady state the original default microphone input was selected again.

Working with a LLM, it looks like this was because fixing #1472 exposed another bug which was hidden due to onActiveDeviceChange not firing:

PreJoin rebuilt both preview tracks on every time a new input device was selected. It passed initialUserChoices.audioDeviceId/videoDeviceId to usePreviewTracks. Despite the name, initialUserChoices is live state that updates whenever the selected device changes. Once selections were reported, every pick changed the usePreviewTracks options, which stopped and recreated both the audio and video tracks (which due to not being exact, reset back to the default), which caused a useEffect to run which wrote the selected track ids to the persistant user data, which was then being parsed and fed back into the initialUserChoices, causing the render loop until the persistent user data state stabilizes.

What I did to fix it

On main, the switch at least worked and only the ui rendered highlight was stale. But this render loop situation is pretty bad, so I pushed up 995a463 to try to fix it:

  • PreJoin: preview tracks are only recreated when audio/video is turned on or off (or the video processor changes). Device ids are read from a ref when a track is created, so device changes go through track.setDeviceId() on the existing track.
  • MediaDeviceSelect: initialSelection is applied once per mount (NOTE: technically this is a breaking change, though it sounds like this maybe was an oversight given the name of the parameter, now it really is purely used in the "initial" place), and skipped when the track is already on that device. This also covers turning the mic off and on, which remounts the menu.
  • useMediaDeviceSelect: activeDeviceId starts from the local track's real device instead of assuming 'default'.

Demo

Video showing current end state of this branch (recorded in chrome, but reproduced in firefox and safari):

Screen.Recording.2026-11-03.at.11.30.24.AM.mov

@vercel

vercel Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
components-js-storybook-5kld Ready Ready Preview Nov 2, 2026 11:10pm UTC

Request Review

This branch was successfully deployed

1 active deployment
Preview — 995a4638 Deployed Nov 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Device selected in PreJoin is not reported as active and is not submitted

3 participants