Developer's Docs
Websocket Streaming
Developer's Docs
Websocket Streaming
How to use our websocket in your application to call your agent
Use the desired personality id under your account in the WebSocket URL.
wss://app.openhome.xyz/websocket/voice-stream/OPENHOME_API_KEY/PERSONALITY_ID
Here is an example:
wss://app.openhome.xyz/websocket/voice-stream/xyzsdsadasannfma/4727
OPENHOME_API_KEY
Checkout API Docs to get OPENHOME_API_KEY
PERSONALITY_ID
Checkout Get Personalities section to get your account’s personality ids.
- 4727: Represents the personality ID.
- Set the personality ID to
0
to skip this part. This will start the call with the default agent, OpenHome.
WebSocket Flow
Audio Data Format
Audio data sent to the WebSocket must adhere to the following specifications:
- Format: 16-bit PCM
- Sample Rate: 16000 Hz
- Encoding: Base64
Ensure that the audio is converted to this format before sending it to the WebSocket.
WebSocket Message Structure
Client to Server Messages
- User to Server Text Messages
{ "data": "MESSAGE_CONTEXT", "type": "transcribed" }
- User to Server Audio Messages
{ "data": "BASE64_ENCODED_AUDIO_MESSAGES", "type": "audio" }
Server to Client Messages
-
Server to User Text Messages
{ "data": { "content": "MESSAGE CONTENT", "live": true, "role": "assistant" }, "type": "message" }
live: true indicates live transcription or response logs.
final: true provides finalized transcription and response messages.
-
Server to User Audio Messages
{ "data": "BASE64_ENCODED_AUDIO_MESSAGES", "type": "audio" }
EXAMPLES
Find More Examples on GitHub
<!DOCTYPE html>
<html>
<head>
<title>Audio Stream</title>
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.status {
padding: 10px;
margin-bottom: 10px;
background-color: #f0f0f0;
}
.active { background-color: #90EE90; }
.error { background-color: #ffcccb; }
#retryButton {
padding: 10px;
margin: 10px 0;
display: none;
}
</style>
</head>
<body>
<div class="container">
<div id="connectionStatus" class="status">Connection Status: Disconnected</div>
<div id="micStatus" class="status">Microphone Status: Off</div>
<div class="call-controls">
<button id="startCall" class="call-button">Start Call</button>
<button id="endCall" class="call-button">End Call</button>
</div>
<button id="retryButton" onclick="retryMicrophoneAccess()">Retry Microphone Access</button>
<audio id="audioPlayer" controls></audio>
</div>
<script>
const api_key = "0603b422xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxd0e87"
const wsUrl = 'wss://app.openhome.xyz/websocket/voice-stream/' + api_key + '/0';
const BUFFER_SIZE = 4096;
const FRAMES_PER_BUFFER = 3200;
const CHANNELS = 1;
const RATE = 16000;
let ws;
let audioContext;
let mediaStream;
let reconnectAttempts = 0;
let microphoneSource;
let audioBuffer = new Float32Array();
// Audio playback setup
let audioElement = document.getElementById('audioPlayer');
let mediaSource = null;
let sourceBuffer = null;
let audioQueue = [];
let isSourceOpen = false;
async function initAudio() {
try {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error('Your browser does not support audio recording');
}
audioContext = new (window.AudioContext || window.webkitAudioContext)({
sampleRate: RATE
});
mediaStream = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: CHANNELS,
sampleRate: RATE,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
if (!mediaStream || !mediaStream.active) {
throw new Error('Failed to get media stream');
}
const tracks = mediaStream.getAudioTracks();
if (!tracks || tracks.length === 0) {
throw new Error('No audio tracks available');
}
microphoneSource = audioContext.createMediaStreamSource(mediaStream);
const processor = audioContext.createScriptProcessor(BUFFER_SIZE, CHANNELS, CHANNELS);
microphoneSource.connect(processor);
processor.connect(audioContext.destination);
processor.onaudioprocess = (e) => {
const inputData = e.inputBuffer.getChannelData(0);
const newBuffer = new Float32Array(audioBuffer.length + inputData.length);
newBuffer.set(audioBuffer);
newBuffer.set(inputData, audioBuffer.length);
audioBuffer = newBuffer;
while (audioBuffer.length >= FRAMES_PER_BUFFER) {
const samplesTo = audioBuffer.slice(0, FRAMES_PER_BUFFER);
const pcmData = convertFloatTo16BitPCM(samplesTo);
if (ws && ws.readyState === WebSocket.OPEN) {
const base64Audio = btoa(String.fromCharCode(...new Uint8Array(pcmData.buffer)));
ws.send(JSON.stringify({
type: 'audio',
data: base64Audio
}));
}
audioBuffer = audioBuffer.slice(FRAMES_PER_BUFFER);
}
};
document.getElementById('micStatus').textContent = 'Microphone Status: Active';
document.getElementById('micStatus').classList.add('active');
document.getElementById('micStatus').classList.remove('error');
document.getElementById('retryButton').style.display = 'none';
if (audioContext.state === 'suspended') {
await audioContext.resume();
}
} catch (error) {
console.error('Error initializing audio:', error);
document.getElementById('micStatus').textContent = 'Microphone Status: Error - ' + error.message;
document.getElementById('micStatus').classList.add('error');
document.getElementById('micStatus').classList.remove('active');
document.getElementById('retryButton').style.display = 'block';
throw error;
}
}
function convertFloatTo16BitPCM(float32Array) {
const int16Array = new Int16Array(float32Array.length);
for (let i = 0; i < float32Array.length; i++) {
const s = Math.max(-1, Math.min(1, float32Array[i]));
int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
}
return int16Array;
}
async function retryMicrophoneAccess() {
try {
if (mediaStream) {
mediaStream.getTracks().forEach(track => track.stop());
}
if (audioContext) {
await audioContext.close();
}
audioBuffer = new Float32Array();
await initAudio();
} catch (error) {
console.error('Error retrying microphone access:', error);
document.getElementById('micStatus').textContent = 'Microphone Status: Error - ' + error.message;
document.getElementById('micStatus').classList.add('error');
}
}
function createMediaSource() {
return new Promise((resolve, reject) => {
try {
if (!isCallActive) {
reject(new Error('Call is not active'));
return;
}
if (mediaSource) {
if (mediaSource.readyState !== 'closed') {
try {
mediaSource.endOfStream();
} catch (e) {
console.error('Error ending previous media source:', e);
}
}
mediaSource = null;
sourceBuffer = null;
}
mediaSource = new MediaSource();
audioElement.src = URL.createObjectURL(mediaSource);
// Set up automatic play when data is available
audioElement.addEventListener('canplay', () => {
audioElement.play().catch(e => console.error('Error playing audio:', e));
});
// Handle audio ending
audioElement.addEventListener('ended', () => {
if (audioQueue.length > 0) {
audioElement.play().catch(e => console.error('Error playing audio:', e));
}
});
mediaSource.addEventListener('sourceopen', () => {
try {
console.log('MediaSource opened');
isSourceOpen = true;
if (!sourceBuffer && mediaSource.readyState === 'open') {
sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
sourceBuffer.mode = 'sequence';
sourceBuffer.addEventListener('updateend', () => {
if (audioQueue.length > 0 && !sourceBuffer.updating) {
processAudioQueue();
}
});
}
resolve();
} catch (e) {
console.error('Error in sourceopen:', e);
reject(e);
}
});
mediaSource.addEventListener('sourceended', () => {
console.log('MediaSource ended');
isSourceOpen = false;
});
mediaSource.addEventListener('sourceclose', () => {
console.log('MediaSource closed');
isSourceOpen = false;
if (isCallActive) {
setTimeout(() => createMediaSource().catch(console.error), 1000);
}
});
} catch (e) {
console.error('Error creating MediaSource:', e);
reject(e);
}
});
}
function processAudioQueue() {
if (!sourceBuffer || !isSourceOpen) return;
while (audioQueue.length > 0 && !sourceBuffer.updating) {
try {
const data = audioQueue.shift();
sourceBuffer.appendBuffer(data);
return;
} catch (e) {
console.error('Error appending buffer:', e);
if (e.name === 'QuotaExceededError') {
if (sourceBuffer.buffered.length > 0) {
const start = sourceBuffer.buffered.start(0);
const end = sourceBuffer.buffered.end(0);
if (end - start > 10) {
sourceBuffer.remove(start, end - 10);
}
}
audioQueue.unshift(data);
return;
}
createMediaSource().catch(console.error);
return;
}
}
}
async function initWebSocket() {
try {
ws = new WebSocket(wsUrl);
ws.onopen = async () => {
console.log('WebSocket connected');
document.getElementById('connectionStatus').textContent = 'Connection Status: Connected';
document.getElementById('connectionStatus').classList.add('active');
document.getElementById('connectionStatus').classList.remove('error');
reconnectAttempts = 0;
await createMediaSource();
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
if (message.type === 'audio' && message.data) {
ws.send(JSON.stringify({
type: "ack",
data: "audio-received"
}));
const audioData = base64ToUint8Array(message.data);
audioQueue.push(audioData);
if (sourceBuffer && !sourceBuffer.updating) {
processAudioQueue();
}
}
} catch (error) {
console.error('Error processing message:', error);
}
};
ws.onclose = () => {
console.log('WebSocket closed');
document.getElementById('connectionStatus').textContent = 'Connection Status: Disconnected';
document.getElementById('connectionStatus').classList.remove('active');
document.getElementById('connectionStatus').classList.add('error');
const timeout = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
reconnectAttempts++;
setTimeout(() => {
initWebSocket();
}, timeout);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
document.getElementById('connectionStatus').textContent = 'Connection Status: Error';
document.getElementById('connectionStatus').classList.remove('active');
document.getElementById('connectionStatus').classList.add('error');
};
} catch (error) {
console.error('Error initializing WebSocket:', error);
document.getElementById('connectionStatus').textContent = 'Connection Status: Error - ' + error.message;
document.getElementById('connectionStatus').classList.add('error');
}
}
function base64ToUint8Array(base64) {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
let isCallActive = false;
const startCallButton = document.getElementById('startCall');
const endCallButton = document.getElementById('endCall');
// Add event listeners for the buttons
startCallButton.addEventListener('click', startCall);
endCallButton.addEventListener('click', endCall);
async function startCall() {
if (isCallActive) return;
try {
startCallButton.disabled = true;
// Reset all states before starting new call
await resetStates();
await init();
isCallActive = true;
startCallButton.style.display = 'none';
endCallButton.style.display = 'block';
audioElement.play().catch(e => console.error('Error playing audio:', e));
} catch (error) {
console.error('Error starting call:', error);
startCallButton.disabled = false;
}
}
async function endCall() {
if (!isCallActive) return;
try {
// Stop the microphone
if (mediaStream) {
mediaStream.getTracks().forEach(track => track.stop());
mediaStream = null;
}
// Close audio context
if (audioContext) {
await audioContext.close();
audioContext = null;
}
// Close WebSocket connection
if (ws) {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
ws = null;
}
await resetStates();
// Update UI
startCallButton.style.display = 'block';
startCallButton.disabled = false;
endCallButton.style.display = 'none';
document.getElementById('connectionStatus').textContent = 'Connection Status: Disconnected';
document.getElementById('connectionStatus').classList.remove('active');
document.getElementById('micStatus').textContent = 'Microphone Status: Off';
document.getElementById('micStatus').classList.remove('active');
} catch (error) {
console.error('Error ending call:', error);
}
}
async function resetStates() {
// Reset MediaSource
if (mediaSource) {
if (mediaSource.readyState !== 'closed') {
try {
mediaSource.endOfStream();
} catch (e) {
console.error('Error ending media stream:', e);
}
}
mediaSource = null;
}
// Clear source buffer
if (sourceBuffer) {
try {
if (!sourceBuffer.updating) {
sourceBuffer.abort();
}
} catch (e) {
console.error('Error aborting source buffer:', e);
}
sourceBuffer = null;
}
// Reset audio element
try {
audioElement.pause();
audioElement.src = '';
audioElement.load(); // Important: properly reset the audio element
} catch (e) {
console.error('Error resetting audio element:', e);
}
// Reset other states
audioQueue = [];
isSourceOpen = false;
audioBuffer = new Float32Array();
isCallActive = false;
reconnectAttempts = 0;
// Return a promise that resolves after a short delay
return new Promise(resolve => setTimeout(resolve, 100));
}
// Modify the init function to not auto-start
async function init() {
try {
await initWebSocket();
await initAudio();
setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
document.addEventListener('click', async () => {
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
}
});
} catch (error) {
console.error('Initialization error:', error);
throw error; // Propagate the error to handle it in startCall
}
}
//init().catch(console.error);
</script>
</body>
</html>