@mrcall/directvoice
v1.0.7
Published
Browser client for MrCall DirectVoice — WebSocket voice calls with an AI assistant. Opus via WebCodecs, zero dependencies.
Maintainers
Readme
MrCallDirectVoice JS SDK
Browser SDK for embedding MrCall DirectVoice AI voice calls on any website. Zero dependencies. Single file. Supports Opus and PCM16.
Bundles
| File | Size | Description |
|------|------|-------------|
| MrCallDirectVoice.bundle.js | ~26 KB | SDK only, debug |
| MrCallDirectVoice.bundle.min.js | ~17 KB | SDK only, minified + obfuscated |
| MrCallDirectVoiceWidget.bundle.js | ~45 KB | Widget + SDK, debug |
| MrCallDirectVoiceWidget.bundle.min.js | ~33 KB | Widget + SDK, minified + obfuscated |
Audio Encoding
| Encoding | Bandwidth | Browser requirement | Description | |----------|-----------|---------------------|-------------| | opus (default) | ~24 kbps | WebCodecs API (Chrome 94+, Edge 94+, Safari 16.4+) | Compressed audio. ~10x smaller than PCM. Uses browser-native WebCodecs encoder/decoder. | | pcm16 | ~384 kbps | Any modern browser | Raw 16-bit PCM. Universal fallback. |
Opus is enabled by default. If WebCodecs is not available, the SDK automatically falls back to PCM16 — no code changes needed.
Widget — One-line Embed
The easiest way to add MrCall DirectVoice to any website. A single <script> tag — no JavaScript needed.
Minimal embed
<script src="MrCallDirectVoiceWidget.bundle.min.js"
data-base-url="https://your-server.com"
data-auth-user="API_USER"
data-auth-password="API_PASS"
data-business-id="YOUR_BUSINESS_ID"
></script>Full embed with customization
<script src="MrCallDirectVoiceWidget.bundle.min.js"
data-base-url="https://your-server.com"
data-auth-user="API_USER"
data-auth-password="API_PASS"
data-business-id="YOUR_BUSINESS_ID"
data-username="end-user-id"
data-email="[email protected]"
data-display-name="Mario Rossi"
data-title="Talk to our AI"
data-bot-name="MrCall Assistant"
data-color="#22c55e"
data-popup-message="Need help? Click to talk!"
data-show-popup="true"
data-show-popup-time="3"
data-position="bottom-right"
></script>Widget data-* attributes
| Attribute | Default | Description |
|-----------|---------|-------------|
| data-base-url | — | Server URL (required) |
| data-ws-url | — | WebSocket path (Option A: backend-initialized) |
| data-auth-user | — | API username (Option B: full call) |
| data-auth-password | — | API password |
| data-business-id | — | CRM business ID |
| data-username | — | End-user identifier |
| data-email | — | End-user email |
| data-display-name | — | End-user display name |
| data-encoding | "opus" | "opus" or "pcm16" |
| data-jitter-buffer-ms | "80" | Audio buffered before playback starts. 0 restores instant playback. |
| data-language | — | ISO language tag (e.g. it-IT, en-US). Overrides business default. |
| data-mode | "floating" | "floating" or "inline" |
| data-position | "bottom-right" | "bottom-right" or "bottom-left" |
| data-target | — | CSS selector for inline mode container |
| data-color | "#22c55e" | Accent color (FAB and call button) |
| data-title | — | Panel header title |
| data-logo-url | MrCall logo | Logo image URL |
| data-bot-name | — | Subtitle under the title |
| data-popup-message | — | Popup bubble text near the FAB |
| data-show-popup | "false" | "true" to show popup |
| data-show-popup-time | "0" | Seconds before popup appears |
| data-auto-open | "false" | "true" to auto-open the panel |
Dynamic user parameters
User details can be set dynamically after the widget is loaded, before the call starts:
window._mrcallWidget.configure({
username: "user123",
email: "[email protected]",
displayName: "Mario Rossi"
});configure() accepts any subset of: username, email, displayName, authUser, authPassword, businessId, wsUrl, baseUrl, encoding, language.
Programmatic usage
const widget = new MrCallDirectVoiceWidget({
baseUrl: "https://your-server.com",
authUser: "API_USER",
authPassword: "API_PASS",
businessId: "YOUR_BUSINESS_ID",
title: "Talk to our AI",
position: "bottom-right",
onCallStarted: (sid) => console.log("Call started:", sid),
onCallEnded: (reason) => console.log("Call ended:", reason),
onError: (msg) => console.error("Error:", msg),
onStatusChange: (status) => console.log("Status:", status)
});
// Update user params at runtime
widget.configure({ username: "john", email: "[email protected]" });
// Teardown
widget.destroy();SDK — Programmatic API
For full control without the UI widget.
Security Architecture
The SDK separates session initialization (requires API credentials) from audio streaming (uses a time-limited access key). This enables two deployment patterns depending on your security requirements.
Pattern A — Backend-initialized (recommended for production)
API credentials stay on your server. The browser never sees them.
Browser Your Backend StarChat
| | |
| "start call" | |
|----------------------->| |
| | POST /voice/init/BID |
| | Authorization: Basic |
| |----------------------->|
| | { sessionId, |
| | accessKey, wsUrl } |
| |<-----------------------|
| { wsUrl } | |
|<-----------------------| |
| |
| WebSocket to wsUrl (no credentials) |
|------------------------------------------------>|
| <-- binary audio (Opus or PCM16) --> |const voice = new MrCallDirectVoice({
baseUrl: 'https://your-starchat-server.com'
});
voice.onCallStarted = (sid) => console.log('Active:', sid);
await voice.startStream(wsUrl); // wsUrl from your backendPattern B — Frontend-initialized (demos, internal tools)
const voice = new MrCallDirectVoice({
authUser: 'apiUser',
authPassword: 'apiPass',
businessId: 'biz-123',
username: 'john',
baseUrl: 'https://your-starchat-server.com'
});
await voice.startCall();Quick Start
1. Include the SDK
<script src="dist/MrCallDirectVoice.bundle.min.js"></script>Or as an ES module:
<script type="module">
import MrCallDirectVoice from './MrCallDirectVoice.js';
</script>AudioWorklet processors and the Opus codec are fully inlined — no extra files to serve.
2. Backend-initialized flow (Pattern A)
Your backend calls the init API:
curl -u apiUser:apiPass -X POST \
"https://host/mrcall/v1/voice/init/BUSINESS_ID?username=john&sampleRate=24000&encoding=opus"Your backend passes wsUrl to the frontend:
const voice = new MrCallDirectVoice({ baseUrl: 'https://starchat-host' });
voice.onCallStarted = (sessionId) => console.log('Call active:', sessionId);
voice.onCallEnded = (reason) => console.log('Call ended:', reason);
voice.onError = (msg) => console.error('Error:', msg);
await voice.startStream(wsUrl);
voice.hangup();3. Frontend-initialized flow (Pattern B)
const voice = new MrCallDirectVoice({
authUser: 'apiUser',
authPassword: 'apiPass',
businessId: 'your-business-id',
username: 'end-user-id',
email: '[email protected]',
displayName: 'John Doe',
encoding: 'opus',
baseUrl: 'https://starchat-host'
});
voice.onCallStarted = () => console.log('Active');
voice.onCallEnded = () => console.log('Ended');
voice.onError = (e) => console.error(e);
await voice.startCall();
voice.hangup();4. Cleanup
window.addEventListener('beforeunload', () => voice.destroy());API Reference
Constructor
new MrCallDirectVoice(options)| Option | Type | Required | Default | Description |
|----------------|--------|-------------------|----------|------------------------------------------|
| baseUrl | string | always | | StarChat server URL (https) |
| encoding | string | no | "opus" | "opus" or "pcm16" |
| language | string | no | | ISO language tag (e.g. it-IT). Overrides business default language for TTS/STT. |
| sampleRate | number | no | 24000 | Audio sample rate: 8000, 16000, or 24000 |
| authUser | string | for startCall() | | API username (Basic Auth) |
| authPassword | string | for startCall() | | API password (Basic Auth) |
| businessId | string | for startCall() | | CRM business identifier |
| username | string | for startCall() | | End-user identifier |
| email | string | no | | End-user email |
| displayName | string | no | | End-user display name |
Methods
| Method | Description |
|--------|-------------|
| startCall() | Full flow. Calls the init API with credentials, then opens the WebSocket. Returns a Promise. |
| startStream(wsUrl) | Stream-only flow. Opens the WebSocket to a pre-obtained wsUrl. No credentials needed. Returns a Promise. |
| hangup() | Sends hangup signal and closes the connection. |
| destroy() | Force-releases all resources (mic, audio, WebSocket, codec). Safe to call at any time. |
| static initSession(opts) | Standalone init call. Returns Promise<{sessionId, accessKey, wsUrl, encoding}>. |
Properties
| Property | Type | Description |
|----------------|---------|-------------------------------------------------------------|
| status | string | "idle" | "connecting" | "active" | "ending" |
| sessionId | string | Server-assigned session ID (available after callStarted) |
| inCall | boolean | true when connecting or active |
| encoding | string | Effective encoding after detection |
| opusSupported | boolean (static) | true if WebCodecs Opus is available |
| version | string (static and instance) | SDK version, e.g. "1.0.6" |
Callbacks
| Callback | Signature | When |
|------------------|------------------------|---------------------------------------|
| onCallStarted | (sessionId) => void | Server confirms call is active |
| onCallEnded | (reason) => void | Call terminated (local or remote) |
| onError | (message) => void | WebSocket or server error |
| onStatusChange | (status) => void | Any status transition |
| onAudioStats | (stats) => void | First few audio frames, each direction |
onAudioStats
Diagnostics for the first audioStatsFrames frames per direction (default 5, 0
disables). It answers the first question worth asking when someone reports a dead
call: is audio not arriving at all, or arriving silent?
voice.onAudioStats = ({ direction, seq, bytes, samples, peak }) => {
console.log(`${direction} #${seq}: ${bytes}B`, samples ? `${samples} samples, peak=${peak}` : "");
};
// send #1: 87B
// recv #1: 960B 480 samples, peak=8241samples and peak are present whenever the PCM is in hand: always on recv, and on
send only in pcm16 mode, since an outgoing Opus frame is already encoded. A peak
of 0 means real audio arrived and it was silence, which is a different fault from no
frames at all. The callback goes quiet after the cap, so it never becomes a per-packet
cost for the life of the call.
Status Lifecycle
idle ──> connecting ──> active ──> ending ──> idle
| |
└── (error) ──> idle └── (error) ──> idleBackend Integration Examples
Node.js / Express
app.post('/api/voice/start', async (req, res) => {
const { username, businessId } = req.body;
const response = await fetch(
`${STARCHAT_URL}/mrcall/v1/voice/init/${businessId}?username=${username}&sampleRate=24000&encoding=opus`,
{
method: 'POST',
headers: { 'Authorization': 'Basic ' + btoa(API_USER + ':' + API_PASS) }
}
);
const session = await response.json();
res.json({ wsUrl: session.wsUrl });
});Python / Flask
@app.route('/api/voice/start', methods=['POST'])
def start_voice():
data = request.json
r = requests.post(
f'{STARCHAT_URL}/mrcall/v1/voice/init/{data["businessId"]}',
params={'username': data['username'], 'sampleRate': 24000, 'encoding': 'opus'},
auth=(API_USER, API_PASS)
)
session = r.json()
return jsonify({'wsUrl': session['wsUrl']})Init API Reference
POST /mrcall/v1/voice/init/{businessId}Auth: Basic HTTP (Authorization header).
Query parameters:
| Param | Required | Description |
|---------------|----------|------------------------------------|
| username | yes | End-user identifier |
| sampleRate | no | 8000, 16000, or 24000 (default) |
| encoding | no | opus (default) or pcm16 |
| email | no | End-user email |
| displayName | no | End-user display name |
| language | no | ISO language tag (e.g. it-IT). Overrides business default. |
Response (200):
{
"sessionId": "abc123-...",
"accessKey": "fa3b2c...128-hex-chars",
"wsUrl": "/mrcall/v1/voice/stream/abc123-.../fa3b2c...?encoding=opus",
"encoding": "opus"
}WebSocket Protocol
URL: wss://{host}/mrcall/v1/voice/stream/{sessionId}/{accessKey}[?encoding=opus]
No authentication headers needed — the access key in the path is the auth.
Binary frames:
- PCM16: Raw little-endian signed 16-bit integer, mono.
- Opus: Raw opus-encoded frames (20ms, mono, 24 kbps VoIP).
Text frames (JSON):
| Direction | Type | Fields | Description |
|-----------|------------------|---------------------|---------------------------------|
| Client | ping | | Keepalive (SDK sends every 5s) |
| Server | pong | | Keepalive response |
| Client | hangup | | End the call |
| Server | callStarted | sessionId | Call is active, start streaming |
| Server | callEnded | reason | Call terminated |
| Server | error | message | Error occurred |
| Server | encodingChanged | encoding | Server fell back to different encoding |
| Server | clearAudio | | Barge-in: drop all buffered playback |
clearAudio arrives when the user speaks over the assistant and the server discards its
outbound audio queue. The SDK handles it automatically: it drops the frames still queued
in the Opus decoder and flushes the playback worklet, keeping a 5ms fade so the stop is
not audible as a click. Without it the assistant would keep talking until the local
buffer drained, which is what makes barge-in feel sluggish on the web compared to a
phone call. A client that ignores the message still works, it just interrupts late.
Build
npm install
npm run buildOutput in dist/:
MrCallDirectVoice.bundle.js/.min.js— SDK onlyMrCallDirectVoiceWidget.bundle.js/.min.js— Widget + SDK
Requirements
- Modern browser with AudioWorklet (Chrome, Edge, Firefox, Safari 14.1+)
- For Opus: WebCodecs API (Chrome 94+, Edge 94+, Safari 16.4+). Falls back to PCM16 automatically.
- HTTPS in production (microphone requires secure context).
localhostandfile://work for testing. - Node.js >= 16 (build only)
Project Structure
MrCallDirectVoice/
MrCallDirectVoice.js # SDK source
MrCallDirectVoiceWidget.js # Widget source (imports SDK)
dist/
MrCallDirectVoice.bundle.js # SDK — debug
MrCallDirectVoice.bundle.min.js # SDK — minified + obfuscated
MrCallDirectVoiceWidget.bundle.js # Widget — debug
MrCallDirectVoiceWidget.bundle.min.js # Widget — minified + obfuscated
mrcall-icon-512x512.png # Default widget logo
sample.html # SDK interactive demo
sample-widget.html # Widget interactive demo
sample-widget-embed.html # Widget one-line embed demo
package.json
rollup.config.js