Environment
client-sdk-swift 2.15.2 (revision 77b5aad0)
- macOS,
AVAudioEngine-backed ADM
- Reproduced via a real app (Cosmo, a voice-AI assistant), not a minimal repro yet — happy to build one if useful
Summary
On macOS, every time the shared AVAudioEngine (re)starts, the SDK unconditionally connects the output (playout) side to the shared system audio device — even when no remote audio track exists yet and nothing needs to render. This causes the engine to claim/reconfigure the shared output device multiple times during a single connect, which is audible as a dip/dropout in whatever else is playing on the same device (e.g., music in another app) at that moment.
What we observed
Instrumented AudioManager.shared's mixer observer (AudioEngineObserver/MixerEngineObserver) with a temporary logging chain that tracks isPlayoutEnabled / isRecordingEnabled / output-connection transitions, then drove real connects end-to-end. Across a single connect (idle → connecting → live), the engine starts with isPlayoutEnabled=true three separate times:
- At mic capture warm-up (
AudioManager.setRecordingAlwaysPreparedMode(true, ...)), before Room.connect() even begins
- Mid-
Room.connect()
- When the remote participant's audio track actually arrives (the one time playout genuinely needs to be ready)
Each of these connects the output node (confirmed via willConnectOutput, 1ch/48kHz/Float32) to the shared device, independent of whether a remote track exists to play.
We suspected this was echo-cancellation reference-signal coupling (i.e. AEC needing the render path live to cancel against it), so we re-ran the same test with AudioProcessingOptions.noProcessing / AudioCaptureOptions.noProcessing — echo cancellation, AGC, and noise suppression all disabled. Identical result: playout is still enabled on every engine start, same 3x pattern. So this isn't AEC-driven; playout appears to be coupled to any engine start unconditionally, regardless of processing configuration.
What we looked for and couldn't find
A way to defer/suppress playout until a remote track genuinely needs to render. startPlayout() / stopPlayout() / initPlayout() exist on the ADM but are only exposed via AudioManager+Testing.swift, explicitly marked "Only internal testing," and not reachable from a normal app target. Nothing on RoomOptions, ConnectOptions, AudioCaptureOptions, or AudioProcessingOptions exposes control over this.
Why this matters
For any app that captures the mic before it's certain remote audio will be needed imminently (prewarming to cut connect latency, or capturing before a remote participant has joined/published), this means the shared output device gets claimed multiple times for no functional reason, which is audible to the user as an interruption to whatever else is playing on the system — independent of any voice-processing/ducking configuration, since it's not gated by that at all.
Ask
Would it be feasible to gate playout-enable on an actual need to render (i.e. only enable it once a remote audio track subscribes, or lazily on first render callback with data), rather than unconditionally on every engine (re)start? Alternatively, exposing a supported way to control this (a RoomOptions/AudioManager flag, or making startPlayout/stopPlayout part of the public API) would let apps that know they don't need playout yet avoid the extra device claims themselves.
Happy to provide more detail, logs, or help build a minimal repro if that's useful.
Environment
client-sdk-swift2.15.2 (revision77b5aad0)AVAudioEngine-backed ADMSummary
On macOS, every time the shared
AVAudioEngine(re)starts, the SDK unconditionally connects the output (playout) side to the shared system audio device — even when no remote audio track exists yet and nothing needs to render. This causes the engine to claim/reconfigure the shared output device multiple times during a single connect, which is audible as a dip/dropout in whatever else is playing on the same device (e.g., music in another app) at that moment.What we observed
Instrumented
AudioManager.shared'smixerobserver (AudioEngineObserver/MixerEngineObserver) with a temporary logging chain that tracksisPlayoutEnabled/isRecordingEnabled/ output-connection transitions, then drove real connects end-to-end. Across a single connect (idle → connecting → live), the engine starts withisPlayoutEnabled=truethree separate times:AudioManager.setRecordingAlwaysPreparedMode(true, ...)), beforeRoom.connect()even beginsRoom.connect()Each of these connects the output node (confirmed via
willConnectOutput, 1ch/48kHz/Float32) to the shared device, independent of whether a remote track exists to play.We suspected this was echo-cancellation reference-signal coupling (i.e. AEC needing the render path live to cancel against it), so we re-ran the same test with
AudioProcessingOptions.noProcessing/AudioCaptureOptions.noProcessing— echo cancellation, AGC, and noise suppression all disabled. Identical result: playout is still enabled on every engine start, same 3x pattern. So this isn't AEC-driven; playout appears to be coupled to any engine start unconditionally, regardless of processing configuration.What we looked for and couldn't find
A way to defer/suppress playout until a remote track genuinely needs to render.
startPlayout()/stopPlayout()/initPlayout()exist on the ADM but are only exposed viaAudioManager+Testing.swift, explicitly marked "Only internal testing," and not reachable from a normal app target. Nothing onRoomOptions,ConnectOptions,AudioCaptureOptions, orAudioProcessingOptionsexposes control over this.Why this matters
For any app that captures the mic before it's certain remote audio will be needed imminently (prewarming to cut connect latency, or capturing before a remote participant has joined/published), this means the shared output device gets claimed multiple times for no functional reason, which is audible to the user as an interruption to whatever else is playing on the system — independent of any voice-processing/ducking configuration, since it's not gated by that at all.
Ask
Would it be feasible to gate playout-enable on an actual need to render (i.e. only enable it once a remote audio track subscribes, or lazily on first render callback with data), rather than unconditionally on every engine (re)start? Alternatively, exposing a supported way to control this (a
RoomOptions/AudioManagerflag, or makingstartPlayout/stopPlayoutpart of the public API) would let apps that know they don't need playout yet avoid the extra device claims themselves.Happy to provide more detail, logs, or help build a minimal repro if that's useful.