Skip to main content

capture_run

Function capture_run 

Source
pub(crate) fn capture_run(
    inner: &Arc<Inner>,
    settings: &Settings,
    callback: &Py<PyAny>,
)
Expand description

Own one whole capture run end to end on a dedicated thread — connect PulseAudio, encode, and deliver — until stopped. It runs on its own thread because the PulseAudio mainloop must be pumped continuously and independently of Python: sharing the caller’s thread would tie capture cadence to the GIL and let any Python stall starve the audio. The body handed to spawn_worker.

Publishes start_state for the handshake (FAILED on any startup error, RUNNING on entering the hot loop) and returns when stop_state leaves STOP_NONE or on a fatal error. The startup sequence, in order:

  1. Seed the mirrors + validate: copies the settings snapshot into the Inner per-frame atomics, and rejects an invalid Opus frame duration, channel count, or sample spec before touching PulseAudio.
  2. Buffer attr / latency: a configured latency_ms uses ADJUST_LATENCY with fragsize set to that latency; otherwise fragsize is floored at ~20 ms, which yields a prompt first frame and avoids PipeWire’s ~2 s default fragment.
  3. Connect + probe: drives the context to Ready on the bounded pump (re-checking stop_pending each turn), and up-front validates a NAMED device via an introspect probe — an async connect_record would not fail synchronously on a bad name. The probe closure is called from C inside mainloop dispatch, so its body is catch_unwind-guarded to keep a panic from unwinding across the FFI boundary.
  4. Encoder + record stream: creates the PcmEncoder (mono/stereo or surround) and drives the record stream to Ready.
  5. Delivery thread: spawns the delivery thread that pops from the DeliveryRing and runs the Python callback there, so GIL stalls cannot back up the PA pump; a callback error is reported as an unraisable exception and never propagates into the loop. The buffer pool is sized to the worst-case body — RED prefix plus a max Opus packet, scaled by stream count for surround (one self-delimited packet per stream).
  6. Hot loop: pumps on the ~20 ms bound, then drains every buffered fragment via peek/discard (a Hole is an xrun — the read index is just advanced), feeding each into RunState. A stop is observed within the pump bound even when the source is wedged. On exit it disconnects the stream, drops the encoder, closes and joins the delivery ring, and reports any dropped stale frames.