Every control on a call follows the same shape: call a mutator,
subscribe to the matching $ observable for state. The server is
the source of truth — a moderator can mute you, the room can lock
itself, the platform can disconnect. Local state (let isMuted = …)
will drift; the observable won’t.
This page covers the pattern. Per-method details live in the reference.
You’ll need an active call. Call controls operate on a Call
instance — get one of these going first.
Install the SDK and create a SignalWire client with a credential provider.
Dial a destination with client.dial() to get a Call instance.
Subscribe to client.session.incomingCalls$ and call.answer().
Every other control is the same shape with different names —
toggleMuteVideo / videoMuted$, toggleDeaf / deaf$,
toggleHandraise / handraised$, etc.
Three properties make this work:
toggleMute mid-flight is
safe — the SDK serializes.$ observables emit on subscribe. The current value arrives
immediately; no wait-for-event step.audioMuted$ emits.Three objects own the controls:
Call — session-level: hangup,
send DTMF, lock,
hold, transfer,
layout (see Layouts).SelfParticipant (call.self) — your own state: mute, deaf,
hand raise, screen share,
audio processing, your volume.Participant (entries in call.participants$) — moderation
actions on other members. Gated by capabilities — see below.The split mirrors server-side authorization: ending a call needs the
end capability, kicking someone needs member.remove, muting
yourself is unconditional.
Mute silences what you send. Deaf silences what you hear. They’re independent — you can be deaf without being muted (you keep talking, but you can’t hear responses). Useful when the user steps away briefly without leaving the room.
Three ways to stop transmitting audio, and they’re not interchangeable:
For instant talk/silence transitions (e.g. holding spacebar), use push-to-talk — mute would feel laggy because the round-trip is visible to the user:
The local audio pipeline also gives you localAudioLevel$ for a
real-time meter and localSpeaking$ for VAD-based speaking
detection — both are observables of the local mic, computed
client-side, fast enough for ~30fps UI updates.
sendDigits only succeeds once status$ is
'connected'. Sending before media is negotiated will fail or be
dropped:
For interactive dialpads (digits sent as the user presses), wire the button click directly — by that point the call is connected.
Methods on other participants exist (participant.mute(),
participant.remove(), participant.setPosition()), but calling them
without the corresponding capability throws server-side. Drive the UI
off SelfCapabilities.member$:
If the flag is false, hide the button. See Capabilities for the full model.
Most toggles resolve cleanly, but three categories can throw — handle them, don’t let an unhandled rejection bubble up:
Only browser-permission errors need a try/catch on each click.
Capability errors shouldn’t be reachable — gate the button instead.
Connection-state errors are prevented by waiting on status$ (see
DTMF, timing matters).
The pure-mute toggles (toggleMute, toggleDeaf) don’t require
any browser permission — they only flip server-side state — so they
won’t throw on permission. They can still throw on capability or
connection state.
Every button uses the same mutator + observable shape:
Volume sliders, audio-processing toggles, the screen-share button, and moderation actions all follow the same shape.
For the full surface — every method and every observable — see the per-class reference:
Participant — mute, deaf, hand raise, audio processing, server-mixed volumes; also the surface for moderation actions on other members (remove, end, setPosition).SelfParticipant — adds enableStudioAudio / disableStudioAudio on top of Participant.WebRTCCall — session-level controls: hangup, sendDigits, toggleLock, toggleHold, transfer, local-mic pipeline (setLocalMicrophoneGain, localAudioLevel$, localSpeaking$, enablePushToTalk, setPushToTalkActive).SelfCapabilities — server-authoritative flags for gating moderation UI; see Capabilities.