Athos Developer Docs
Browser SDK

Microphone & audio

Device selection, mute, autoplay recovery, and reconnect behavior.

The SDK manages the microphone and the audio element for you. It auto-creates the <audio> element that plays the persona's voice and appends it to the page — you don't manage playback yourself. The page must be served over HTTPS (or localhost): browsers withhold microphone access entirely on a plain http:// origin, which the SDK reports as NO_MIC_AVAILABLE (see Requirements).

Permission is requested before the call starts

connect() asks the browser for the microphone before it redeems the session token. Denying that first prompt costs nothing: the token is not spent, no call is created, and nothing is billed — so a rep who blocks the mic cannot leave a charged session that never starts.

That guarantee covers the pre-connect prompt only. The same codes can also be raised after redemption — a mic unplugged in the moment between the permission check and the call going live — and from the code alone you cannot tell which happened. So retry the token once, and mint a fresh one only if that retry says the token is gone. Create a new session for every attempt; a session is finished once you call disconnect() or its call has ended:

import { AthosRoleplay, AthosRoleplayError } from '@useathos/sdk';

const MIC_CODES = ['MIC_PERMISSION_DENIED', 'NO_MIC_AVAILABLE', 'MIC_DEVICE_DISCONNECTED'];

// Called from your "start call" button, and again from a "try again" button.
async function start(token: string) {
  const session = AthosRoleplay.create({ token, drillKey: 'ma-full-sale' });
  session.on('ready', showCallUI);
  try {
    await session.connect();
    return session;
  } catch (err) {
    // A caught value is `unknown` under strict TypeScript — narrow before reading `.code`.
    if (err instanceof AthosRoleplayError) {
      if (MIC_CODES.includes(err.code)) {
        // Don't loop: getUserMedia won't behave differently until the rep acts.
        showMicHelp({ retryWith: token });
      } else if (err.code === 'TOKEN_ALREADY_USED') {
        showMicHelp({ retryWith: await mintToken() }); // the failure came later
      }
    }
    throw err;
  }
}

A browser leaves the permission prompt pending for as long as the user ignores it — it neither resolves nor rejects. connect() stays unsettled and the session stays in connecting until they answer or dismiss it. Show your own "waiting for microphone access" state on connecting; don't wait on ready to decide the call has started.

Cancelling while the prompt is open

If the rep changes their mind, call session.disconnect(). Nothing further starts: the token is not redeemed, no call is created, the microphone is released, and the session goes silent — no ready, no error, not even for a prompt the rep denies afterwards.

Treat your own disconnect() call as the end of the attempt. Do not wait on the pending connect() promise to tell you the cancel finished — it cannot settle until the browser prompt is answered, which may be never:

cancelButton.onclick = () => {
  session.disconnect();
  clearCallUI();          // now, not in a .then() on connect()
};

That session object is spent either way — start the next attempt with a new one.

Selecting a microphone

const mics = await session.listMicrophones(); // [{ deviceId, label }]
await session.setMicrophone(mics[0].deviceId); // switch mid-call, no drop

listMicrophones() may prompt the user for microphone permission the first time. setMicrophone() switches the active input without dropping the call — wire it to a settings dropdown.

Mute

await session.mute();
await session.unmute();

Autoplay recovery

Browsers block audio autoplay until the user interacts with the page. If playback is blocked, the SDK emits an error with code AUDIO_PLAYBACK_BLOCKED. Recover by calling resumeAudio() from within a user-gesture handler (a click) — browsers only unblock audio then.

session.on('error', ({ code }) => {
  if (code === 'AUDIO_PLAYBACK_BLOCKED') showResumeButton();
});

// in your button's click handler:
resumeButton.onclick = () => session.resumeAudio();

Reconnect behavior

On a transient network drop the SDK reconnects automatically:

  drop ──▶ reconnecting ──▶ reconnected        (recovered)
                       └──▶ error: NETWORK_LOST (after 30s, session ends)
  • It emits reconnecting, then reconnected on recovery.
  • If recovery does not succeed within 30 seconds, it emits an error with code NETWORK_LOST and ends the session.

Show a non-blocking banner on reconnecting and clear it on reconnected; treat NETWORK_LOST as a terminal error and offer to restart.

Microphone errors

CodeMeaning
MIC_PERMISSION_DENIEDThe user denied microphone permission. Prompt them to allow it.
MIC_DEVICE_DISCONNECTEDThe active mic was unplugged, or is held by another app.
NO_MIC_AVAILABLENo microphone is available, or the page is not on HTTPS.

Raised from connect(), the token is usually unspent — always so for a permission the rep never granted, which is the common case. Raised mid-call, the session stays live and billable: the SDK only emits the error, so call setMicrophone() to switch devices or disconnect() to end the call yourself. Only NETWORK_LOST ends a session on its own.

See the full list in Error codes.

On this page