Skip to content

Advanced Audio Controls #503

Description

@muke1908

Goal

Add advanced call audio controls to the SDK: mute/unmute, input device switching, output device selection, and an audio level meter.

Applications should be able to build a real call UI on top of the SDK instead of only starting and ending calls.

Why this matters

A usable voice-call experience needs more than a peer connection. Users expect to mute themselves, change microphones, select speakers/headphones, and see whether their microphone is active.

These controls also teach important WebRTC media-track lifecycle concepts.

Functional requirements

  • Add mute/unmute controls for local microphone audio.
  • Add a setMuted(boolean) style API for explicit state control.
  • Expose current mute state.
  • Add input device switching by device id.
  • Use RTCRtpSender.replaceTrack() when switching microphones to avoid rebuilding the call.
  • Stop old tracks after switching devices.
  • Add output device selection where supported by the browser.
  • Add audio level meter support for local input, and remote level when stats/browser support allows it.
  • Handle unsupported browser APIs gracefully.
  • Clean up media streams, tracks, audio elements, and audio contexts on call end.

Suggested API

const call = await chat.startCall();

call.mute();
call.unmute();
call.setMuted(true);
console.log(call.muted);

await call.switchInputDevice('microphone-device-id');
await call.setOutputDevice('speaker-device-id');

const metrics = await call.getStatsSnapshot();
console.log(metrics.localAudioLevel);

Learning material

This issue teaches how WebRTC audio tracks and devices work.

Muting with MediaStreamTrack.enabled

The common way to mute a microphone in WebRTC is:

  • keep the track alive
  • set track.enabled = false

This sends silence/disabled media behavior without tearing down the sender. Unmuting sets track.enabled = true.

This is different from stopping a track.

Stopping a track

Calling track.stop() permanently ends that track. It is useful during cleanup or after replacing an old microphone track, but it is not ideal for a temporary mute button.

Switching microphones with replaceTrack()

To change input devices during a call:

  1. Call navigator.mediaDevices.getUserMedia() with the new audio device id.
  2. Extract the new audio track.
  3. Find the existing audio RTCRtpSender.
  4. Call sender.replaceTrack(newTrack).
  5. Stop the old track.

This avoids renegotiating the entire call in many common cases.

Output device selection

Some browsers support HTMLMediaElement.setSinkId(deviceId), which lets an app choose the speaker/headset output device.

Important browser reality:

  • not all browsers support setSinkId
  • it may require HTTPS/secure context
  • permission and device availability can change
  • SDK should throw a clear error or no-op gracefully when unsupported

Audio level meter

A local audio meter can be built using the Web Audio API:

  • create an AudioContext
  • create a MediaStreamSource from the local stream
  • connect it to an AnalyserNode
  • read time-domain samples
  • calculate RMS volume level

Remote audio level may also be available through WebRTC stats, depending on browser support.

Device lifecycle

Media devices are dynamic:

  • microphones can be unplugged
  • Bluetooth devices can disconnect
  • browser permissions can be revoked
  • selected device ids may become invalid

The SDK should fail clearly and clean up old/new tracks carefully.

Implementation notes

  • Keep mute/unmute local to outgoing audio tracks.
  • Do not call track.stop() for normal mute.
  • Preserve mute state when switching input devices.
  • Prefer replaceTrack() over adding a second audio sender.
  • Avoid leaking stopped tracks or dangling AudioContext instances.
  • Extend the audio sink abstraction for output-device selection.
  • Treat audio level fields as optional because browser support differs.
  • Document browser support caveats.

Testing ideas

  • Verify mute() sets local audio track enabled = false.
  • Verify unmute() sets local audio track enabled = true.
  • Verify setMuted(true/false) is idempotent.
  • Verify switchInputDevice() calls getUserMedia() with the selected device id.
  • Verify replaceTrack() is called with the new track.
  • Verify old tracks are stopped after successful replacement.
  • Verify mute state is preserved after switching devices.
  • Verify setOutputDevice() delegates to setSinkId() when available.
  • Verify unsupported output selection produces a clear failure.
  • Verify cleanup stops tracks and detaches audio on call end.

Acceptance criteria

  • Active calls expose mute/unmute/setMuted APIs.
  • Active calls expose current mute state.
  • Active calls can switch input microphone device.
  • Input switching uses replaceTrack() where possible.
  • Old input tracks are stopped after switching.
  • Active calls can select output device where browser support exists.
  • Audio level is available through diagnostics/metrics when supported.
  • Tests cover mute, unmute, input switching, output switching, level meter behavior, and cleanup.
  • Docs explain media track lifecycle and browser support caveats.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions