For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
Log inSign up
Support
GuidesReferenceClick-to-Call
GuidesReferenceClick-to-Call
  • Getting Started
    • Overview
    • Authentication
    • RxJS Primer
    • Migrate from v3
  • Web Components
    • Overview
    • Click-to-Call
    • Theming
    • Customization
  • Build Voice & Video Apps
    • Overview
    • Outbound Calls
    • Inbound Calls
    • Device Management
    • Screen Sharing
    • Call Controls
    • Layouts
    • Messaging & Chat
  • Manage Resources
    • Overview
    • Users
    • Address Book
    • Client Preferences
    • Capabilities
  • Deploy
    • Overview
    • Framework Integration
    • SSR & Next.js
    • Troubleshooting
LogoLogoSignalWire Docs
Log inSign up
Support
On this page
  • The pattern
  • Where each control lives
  • Mute vs. deaf vs. hold
  • Mute vs. deaf
  • Mute vs. hold vs. push-to-talk
  • DTMF, timing matters
  • Moderation — check the capability first
  • Handling unsuccessful attempts
  • Example: control bar
  • Reference
Build Voice & Video Apps

Call Controls

|View as Markdown|Open in Claude|
Was this page helpful?
Edit this page
Previous

Layouts & Participant Views

Next
Built with

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.

Set up the client

Install the SDK and create a SignalWire client with a credential provider.

Place an outbound call

Dial a destination with client.dial() to get a Call instance.

Answer an inbound call

Subscribe to client.session.incomingCalls$ and call.answer().

The pattern

1call.self$.subscribe((self) => {
2 if (!self) return;
3
4 // Trigger
5 muteBtn.onclick = () => self.toggleMute();
6
7 // Reflect — fires once with the current state, then on every change
8 self.audioMuted$.subscribe((muted) => {
9 muteBtn.classList.toggle("muted", muted);
10 muteBtn.textContent = muted ? "Unmute" : "Mute";
11 });
12});

Every other control is the same shape with different names — toggleMuteVideo / videoMuted$, toggleDeaf / deaf$, toggleHandraise / handraised$, etc.

Three properties make this work:

  • Toggles are idempotent. Calling toggleMute mid-flight is safe — the SDK serializes.
  • $ observables emit on subscribe. The current value arrives immediately; no wait-for-event step.
  • The mutator only triggers the change; the observable closes the loop. A moderator-initiated mute that never went through the button still updates the UI, because audioMuted$ emits.

Where each control lives

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 vs. deaf vs. hold

Mute vs. deaf

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.

Mute vs. hold vs. push-to-talk

Three ways to stop transmitting audio, and they’re not interchangeable:

ActionWhat it doesLatencyUse for
toggleMuteDisables the audio track server-sideRound-tripStandard mute button
toggleHoldPauses media transmission for the whole callRound-trip”Be right back” / call park
Push-to-talk (local pipeline)Sets local mic gain to 0 — track stays aliveInstant (no round-trip)Walkie-talkie UIs

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:

1call.enablePushToTalk();
2document.addEventListener("keydown", (e) => {
3 if (e.code === "Space") call.setPushToTalkActive(true);
4});
5document.addEventListener("keyup", (e) => {
6 if (e.code === "Space") call.setPushToTalkActive(false);
7});

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.

DTMF, timing matters

sendDigits only succeeds once status$ is 'connected'. Sending before media is negotiated will fail or be dropped:

1import { filter, take } from "rxjs";
2
3call.status$
4 .pipe(filter((s) => s === "connected"), take(1))
5 .subscribe(async () => {
6 await call.sendDigits("1234#");
7 });

For interactive dialpads (digits sent as the user presses), wire the button click directly — by that point the call is connected.

Moderation — check the capability first

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$:

1call.self?.capabilities.member$.subscribe((member) => {
2 kickButton.hidden = !member.remove;
3 muteOthersButton.disabled = !member.muteAudio.on;
4});

If the flag is false, hide the button. See Capabilities for the full model.

Handling unsuccessful attempts

Most toggles resolve cleanly, but three categories can throw — handle them, don’t let an unhandled rejection bubble up:

SourceWhen it throwsWhat to do
Browser permissionunmuteVideo() or selectVideoInputDevice() after the user denied camera access at the OS levelCatch NotAllowedError and prompt the user to re-enable in settings
Server capabilityModeration methods (participant.mute(), participant.remove()) without the right capabilityGate the button on SelfCapabilities — don’t catch, just hide it
Connection statesendDigits() before status$ is 'connected', anything after the call has endedSubscribe to status$ and gate calls on 'connected'

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).

1muteVideoBtn.onclick = async () => {
2 try {
3 await call.self.toggleMuteVideo();
4 } catch (err) {
5 if (err?.name === "NotAllowedError") {
6 showToast("Camera access is blocked. Allow camera in your browser settings.");
7 return;
8 }
9 console.error("Failed to toggle video:", err);
10 }
11};

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.

Example: control bar

Every button uses the same mutator + observable shape:

1call.self$.subscribe((self) => {
2 if (!self) return;
3
4 // Triggers
5 muteBtn.onclick = () => self.toggleMute();
6 videoBtn.onclick = () => self.toggleMuteVideo();
7 deafBtn.onclick = () => self.toggleDeaf();
8 handBtn.onclick = () => self.toggleHandraise();
9 hangupBtn.onclick = () => call.hangup();
10
11 // Reflections
12 self.audioMuted$.subscribe((m) => muteBtn.classList.toggle("muted", m));
13 self.videoMuted$.subscribe((m) => videoBtn.classList.toggle("muted", m));
14 self.deaf$.subscribe((d) => deafBtn.classList.toggle("active", d));
15 self.handraised$.subscribe((h) => handBtn.classList.toggle("active", h));
16});

Volume sliders, audio-processing toggles, the screen-share button, and moderation actions all follow the same shape.

Reference

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.