Skip to content

Unplugging the microphone aborts the process (publish_tracks requires a removed MicrophoneDevice) #1055

Description

@robtfm

Summary

Unplugging (or otherwise losing) the default microphone while the mic is enabled aborts the whole process with:

thread 'main' panicked at 'Encountered an error in system `comms::livekit::mic::publish_tracks`:
Parameter `Res<'_, MicrophoneDevice>` failed validation: Resource does not exist

Hit live on macOS by physically unplugging a mic mid-run. Affects the desktop client (a mic unplug crashes the app) and is worse on the headless authoritative server, where an abort takes every co-tenant scene down with it.

Mechanism

All the mic systems are .chain()ed in Update (crates/comms/src/livekit/mic.rs:75-95), in this order:

  1. verify_microphone_device_healthrun_if(in_state(MicrophoneAvailability::Available))
  2. ...
  3. publish_tracksrun_if(in_state(MicrophoneAvailability::Available).and(in_state(MicrophonePermission::Granted)))

When the device disappears, verify_microphone_device_health (:190-201) does both of:

commands.set_state(MicrophoneAvailability::Unavailable);
commands.remove_resource::<MicrophoneDevice>();

These two land at different times:

  • the resource removal is a command, applied at the sync point .chain() inserts — i.e. before publish_tracks runs, in the same frame;
  • the state transition only takes effect when the StateTransition schedule runs, i.e. at the next frame boundary.

So for exactly one frame the resource is gone while in_state(Available) still evaluates true. publish_tracks is scheduled, its Res<MicrophoneDevice> fails param validation, and the process aborts. Note the query filters inside publish_tracks are irrelevant — param validation happens before the system body, so an empty participant query does not save it.

This should reproduce every time the device is lost while MicrophoneState::Enabled + MicrophonePermission::Granted, not intermittently.

Suggested fix

Minimal and ordering-independent — take the resource optionally and bail:

fn publish_tracks(
    // ...
    microphone_device: Option<Res<MicrophoneDevice>>,
) {
    let Some(microphone_device) = microphone_device else {
        return;
    };

That removes the assumption that state and resource lifetime stay in lockstep, rather than trying to make the two commands land in the same frame.

Related

The headless server has no use for a microphone at all. HEADLESS_TOWEROFMADNESS_TEST.md §2b.5 ("audio gate") already called for gating mic/kira behind a resource so the server does not pull in cpal/kira; that never landed. Doing so would make this unreachable server-side, but the client fix above is still wanted on its own.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions