broad-boots-9735
02/13/2026, 3:37 PMtall-airline-16756
02/13/2026, 3:38 PMtall-airline-16756
02/13/2026, 3:38 PMagent_name and a room name (the room will be created if it doesn’t exist).
• Example (Python async):
from livekit import api
async def dispatch_agent():
lk_api = api.LiveKitAPI(
url="your_livekit_url",
api_key="your_api_key",
api_secret="your_api_secret"
)
await lk_api.agent_dispatch.create_dispatch(
api.CreateAgentDispatchRequest(
agent_name="your-agent-name",
room="my-call-room",
metadata='{"user": "Alice"}' # optional
)
)
await lk_api.aclose()
2) Generate a participant token for your application
• Your app needs an access token that grants room_join for the same room. Issue this token server-side using your API key/secret.
• Example token creation (Python):
from livekit import api as server_api
token = (
server_api.AccessToken("your_api_key", "your_api_secret")
.with_identity("user-123")
.with_grants(server_api.VideoGrants(room_join=True, room="my-call-room"))
.to_jwt()
)
• Return this token to your client app (web/mobile) and use it to connect to LiveKit.
3) Connect your app to the room
• In your client (WebRTC) code, connect to your LiveKit server with the generated token. Once connected, publish your microphone/audio track and subscribe to the agent’s audio.
• The agent and your app will exchange audio and data in real time inside the room.
4) Interaction notes / improvements
• The agent handles STT → LLM → TTS automatically once dispatched; your app only needs to provide audio and receive audio.
• Use preemptive_generation=True in agent session configuration if you want smoother, faster responses from the agent.
Summary
• Dispatch the agent to a room via AgentDispatch.
• Generate a participant token that allows your app to join that same room.
• Connect your app with that token and exchange audio/data with the agent in real time.
• No SIP provider is required — everything runs over LiveKit rooms (WebRTC).
If you’d like, tell me:
• The language/platform of your application (Web, iOS, Android, Node, Python, etc.) so I can give a short client example for joining the room, or
• Your agent’s agent_name and the room name you want to use and I’ll draft the exact API calls (with placeholders) you can run. Sources: Agents telephony integration | LiveKit Documentation | Agent dispatch | LiveKit Documentation | Outbound Calling Script | LiveKit Documentation | server-sdk-kotlin | livekit/livekit_composite | DeepWiki | Survey Calling Agent | LiveKit Documentationbroad-boots-9735
02/13/2026, 3:52 PMtall-airline-16756
02/13/2026, 3:53 PMbroad-boots-9735
02/13/2026, 3:53 PMtall-airline-16756
02/13/2026, 3:54 PMTOKEN and ROOM_URL with your values.
1) Install dependencies
pip install livekit pyaudio sounddevice numpy
• livekit is the LiveKit Python client SDK.
• sounddevice and numpy are used to play incoming audio.
• (On some systems you may prefer pyaudio for capture; example below uses sounddevice for playback and the SDK’s built-in capture helper if available.)
2) Full example (save as client.py)
import asyncio
import os
import sys
import numpy as np
import sounddevice as sd
from livekit import Room, LocalAudioTrack, Track
from livekit.utils import AudioFrame
# Replace these with your actual values
TOKEN = os.getenv("LIVEKIT_TOKEN", "YOUR_PARTICIPANT_JWT_HERE")
ROOM_URL = os.getenv("LIVEKIT_URL", "<wss://your-livekit-instance.com>")
# Playback parameters (LiveKit audio frames are float32 PCM)
SAMPLE_RATE = 48000
CHANNELS = 1
BUFFER_SECONDS = 0.2 # small buffer for playback
def play_audio_buffer(audio_frame: AudioFrame):
"""
audio_frame.samples is a bytes-like PCM float32 interleaved buffer
The LiveKit Python SDK delivers frames as numpy arrays in many builds,
but this function handles a raw numpy float32 array.
"""
try:
data = audio_frame.data # SDK may expose frame data as numpy array
except AttributeError:
# Fallback: try raw samples property
data = np.frombuffer(audio_frame.samples, dtype=np.float32)
# Ensure correct shape for sounddevice (frames, channels)
if data.ndim == 1 and CHANNELS == 1:
sd.play(data, samplerate=SAMPLE_RATE, blocking=False)
else:
# If interleaved, reshape: (n_frames, channels)
frames = data.reshape(-1, CHANNELS)
sd.play(frames, samplerate=SAMPLE_RATE, blocking=False)
async def main():
if TOKEN == "YOUR_PARTICIPANT_JWT_HERE":
print("Please set TOKEN to your participant JWT (or set LIVEKIT_TOKEN env var).")
sys.exit(1)
room = Room()
# Optional: event handlers
@room.on("participant_connected")
def on_participant_connected(participant):
print(f"Participant connected: {participant.identity}")
@room.on("participant_disconnected")
def on_participant_disconnected(participant):
print(f"Participant disconnected: {participant.identity}")
@room.on("track_subscribed")
def on_track_subscribed(track: Track, publication, participant):
print(f"Subscribed to track {publication.track_sid} from {participant.identity}, kind={track.kind}")
# If this is audio, attach a callback to receive audio frames
if track.kind == track.Kind.KIND_AUDIO:
# The LiveKit Python SDK may expose a `subscribe` callback for audio frames.
# Use `on_frame` if available, otherwise use generic `track` APIs from the SDK.
try:
@track.on("frame")
def on_audio_frame(frame):
# frame is an AudioFrame-like object
play_audio_buffer(frame)
except Exception:
print("Audio frame callback could not be attached; check SDK version for frame API.")
@room.on("track_unsubscribed")
def on_track_unsubscribed(track, publication, participant):
print(f"Unsubscribed from track {publication.track_sid} from {participant.identity}")
# Connect to the room
print("Connecting to room...")
await room.connect(ROOM_URL, TOKEN)
print("Connected as:", room.local_participant.identity)
# Create and publish a local microphone audio track (if supported on platform)
try:
print("Creating local audio track (microphone)...")
# The SDK offers helpers to create a LocalAudioTrack from system microphone.
# This call may vary by SDK version. If not available, capture and publish frames manually.
local_audio: LocalAudioTrack = await room.create_local_audio_track() # helper method
await room.local_participant.publish_track(local_audio)
print("Published local audio track.")
except Exception as e:
print("Could not create/publish local mic track automatically:", e)
print("If you want microphone capture, implement manual capture and publish frames per SDK docs.")
try:
# Keep the script running until disconnected
await room.disconnected.wait()
print("Disconnected from room.")
finally:
await room.disconnect()
if __name__ == "__main__":
asyncio.run(main())
Notes and troubleshooting
• The exact names/APIs for audio frame callbacks and create_local_audio_track() can vary slightly between LiveKit Python SDK versions. If you get attribute errors:
◦ Check your installed livekit package version.
◦ If the SDK lacks create_local_audio_track(), capture microphone audio with sounddevice and publish frames via the SDK’s Push-based track APIs (I can provide an example if needed).
◦ If audio frames come in a different object shape, print the frame object to inspect available fields (e.g., print(dir(frame))).
Next steps I can help with
• If you want a robust mic-capture + publish example (manual capture loop with sounddevice and publishing PCM frames to LiveKit), tell me and I’ll produce the code.
• If you hit a specific AttributeError or event name mismatch, paste the error and I’ll adapt the example to your installed SDK version.
Would you like the manual microphone capture + publish example or help matching this code to your SDK version?
Sources: SDKs | livekit/livekit_composite | DeepWiki | server-sdk-js | livekit/livekit_composite | DeepWikibroad-boots-9735
02/13/2026, 3:56 PMfrom livekit import Room, LocalAudioTrack, Track
from livekit.utils import AudioFrametall-airline-16756
02/13/2026, 3:57 PMRoom, LocalAudioTrack, etc. The Python packages on PyPI are server-side (token/room management), not WebRTC clients.
What this means for your flow
• Keep your server-side Python code that generates the participant token — that part is correct and stays in Python.
• For the client that connects to the room, captures microphone audio, and plays the agent’s audio, use a LiveKit client SDK that supports WebRTC (the recommended option is the JavaScript/Browser SDK or one of the native SDKs such as iOS/Android/React Native).
• You cannot run a native LiveKit WebRTC client purely in Python (except via complex workarounds like driving a headless browser).
I can help in two practical ways — pick one: 1) Provide a minimal, complete example with:
• A small Python server (Flask/FastAPI) that exposes your existing token endpoint, and
• A browser client (HTML + JavaScript) using the official livekit-client SDK that:
◦ Requests the token,
◦ Connects to the room,
◦ Publishes the microphone,
◦ Plays back the agent’s audio.
2) Provide a headless/browser-driven approach if you must keep everything under Python (using Playwright or Selenium to run the JS client from Python). Note: this is more brittle and not recommended for production.
If you want option 1 (recommended), tell me:
• Do you prefer Flask or FastAPI for the Python server?
• Your LiveKit server URL and the token endpoint path you already have (or tell me to use placeholders).
I’ll generate the full server + browser client code with clear instructions and exact placeholders to drop in your token-creation code.
If you prefer option 2, I’ll outline the Playwright approach and show the full Python script that launches a headless browser running the official JS client.
Sources: client-sdk-js | livekit/livekit_composite | DeepWiki | Agents (Python) | livekit/livekit_composite | DeepWiki | Python SDKs | livekit/livekit_composite | DeepWikibroad-boots-9735
02/13/2026, 3:58 PMtall-airline-16756
02/13/2026, 3:58 PMlivekit-client JS SDK to join the room, publish microphone audio, and play incoming audio (including the agent).
Replace placeholders (LIVEKIT_URL, API_KEY/SECRET, AGENT_NAME, ROOM_NAME) with your values.
1) Python server (Flask) — token endpoint
• Install:
pip install flask livekit
• Save as server.py:
# server.py
import os
from flask import Flask, jsonify
from livekit import AccessToken, VideoGrants # server-side token generation
app = Flask(__name__)
# Replace with your LiveKit API key/secret and URL
API_KEY = os.getenv("LIVEKIT_API_KEY", "YOUR_API_KEY")
API_SECRET = os.getenv("LIVEKIT_API_SECRET", "YOUR_API_SECRET")
LIVEKIT_URL = os.getenv("LIVEKIT_URL", "<https://your-livekit-instance.com>") # for info only
@app.route("/token/<identity>/<room>")
def token(identity, room):
# Generate a participant token that can join `room`
at = AccessToken(API_KEY, API_SECRET)
at.with_identity(identity)
at.with_grants(VideoGrants(room_join=True, room=room))
jwt = at.to_jwt()
return jsonify({"token": jwt, "livekit_url": LIVEKIT_URL})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
How to use:
• Start the server: python server.py
• Request a token (example): GET http://localhost:5000/token/user123/my-call-room
• That endpoint returns JSON with token. Your browser client will fetch that.
2) Browser client (HTML + JS) — connect + mic publish + play audio
• No build step required. Uses CDN for livekit-client.
• Save as client.html in same folder (or serve statically):
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>LiveKit Agent Client</title>
<script src="https://unpkg.com/livekit-client@latest/dist/livekit-client.umd.js"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
button { margin: 5px; }
</style>
</head>
<body>
<h3>LiveKit Agent Client</h3>
<label>Identity: <input id="identity" value="user123" /></label><br/>
<label>Room: <input id="room" value="my-call-room" /></label><br/>
<button id="join">Join Room</button>
<button id="leave" disabled>Leave Room</button>
<p id="status"></p>
<script>
const { connect, Room } = livekitClient;
const serverBase = window.location.origin; // assumes server is same origin; adjust if needed
const joinBtn = document.getElementById('join');
const leaveBtn = document.getElementById('leave');
const status = document.getElementById('status');
let room;
joinBtn.onclick = async () => {
const identity = document.getElementById('identity').value;
const roomName = document.getElementById('room').value;
status.textContent = 'Requesting token...';
// Call your Python server to get token
const res = await fetch(`${serverBase}/token/${encodeURIComponent(identity)}/${encodeURIComponent(roomName)}`);
if (!res.ok) {
status.textContent = 'Token request failed';
return;
}
const data = await res.json();
const token = data.token;
const livekitUrl = data.livekit_url || (window.location.origin.replace(/^http/, 'ws') ); // not required for connect()
status.textContent = 'Connecting to room...';
try {
// Connect to LiveKit
room = await connect(token, {
// server may be defaulted; to force URL: connect(livekitUrl, token)
// If you need to supply the ws URL explicitly, use connect("<wss://your-livekit-instance>", token, options)
// For most hosted LiveKit, provide the ws/wss URL and the token
// Example: await connect("<wss://your-livekit-instance.com>", token);
// Here we call connect with token only when the browser can infer the URL from the token
// For safety, you can call: await connect("<wss://your-livekit-instance.com>", token);
}, token);
status.textContent = 'Connected as: ' + room.localParticipant.identity;
joinBtn.disabled = true;
leaveBtn.disabled = false;
// Publish microphone
const pc = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
const audioTrack = pc.getAudioTracks()[0];
const localTrack = await livekitClient.LocalAudioTrack.createFromMediaStreamTrack(audioTrack);
await room.localParticipant.publishTrack(localTrack);
status.textContent += ' — Microphone published';
// Listen for remote participant (agent) audio
room.on('participantConnected', participant => {
status.textContent = `Participant connected: ${participant.identity}`;
});
room.on('trackPublished', (publication, participant) => {
console.log('trackPublished', publication, participant);
});
room.on('trackSubscribed', (track, publication, participant) => {
console.log('trackSubscribed', publication.trackSid, participant.identity, track.kind);
if (track.kind === 'audio') {
// attach audio element to play remote audio
const el = document.createElement('audio');
el.autoplay = true;
el.controls = false;
el.srcObject = track.mediaStream;
document.body.appendChild(el);
}
});
} catch (err) {
console.error(err);
status.textContent = 'Connection failed: ' + err.message;
}
};
leaveBtn.onclick = async () => {
if (room) {
room.disconnect();
status.textContent = 'Left room';
joinBtn.disabled = false;
leaveBtn.disabled = true;
}
};
</script>
</body>
</html>
Important notes and adjustments
• If your Python token endpoint runs on a different origin, set serverBase to that server URL (e.g., http://localhost:5000).
• The JS connect call typically takes <wss://your-livekit-instance> and the token. Some hosted setups accept the token alone; if you get errors, call connect("<wss://your-livekit-instance.com>", token).
• The code uses LocalAudioTrack.createFromMediaStreamTrack from the JS SDK. If the SDK version differs, use Room.localParticipant.publishTrack(new LocalAudioTrack(track)) per SDK docs.
• The agent will be dispatched by your server or via your earlier API call; ensure the agent is in the same room (my-call-room). Once the agent is in the room, your client subscribes to its audio and should hear it.
Optional: Dispatching agent automatically
• If you want the server to automatically dispatch your agent when a client requests a token, you can call AgentDispatch.create_dispatch from your server before returning the token. You already have that working; ensure the same room is used.
If you want, I can now:
• Provide a version that explicitly calls connect("wss://...your-livekit-url...", token) (tell me your LiveKit URL), or
• Convert the server example to FastAPI, or
• Show how to dispatch the agent from the same Flask endpoint before returning the token (so agent always joins before the client).
Which of these would you like next?broad-boots-9735
02/13/2026, 4:01 PMtall-airline-16756
02/13/2026, 4:01 PMbroad-boots-9735
02/13/2026, 4:02 PMtall-airline-16756
02/13/2026, 4:02 PM# generate_token.py
from livekit import AccessToken, VideoGrants
import os
API_KEY = os.getenv("LIVEKIT_API_KEY", "YOUR_API_KEY")
API_SECRET = os.getenv("LIVEKIT_API_SECRET", "YOUR_API_SECRET")
at = AccessToken(API_KEY, API_SECRET)
at.with_identity("local-user")
at.with_grants(VideoGrants(room_join=True, room="my-call-room"))
print(at.to_jwt())
Run: python generate_token.py and copy the printed token.
2) Save this file as client.html and open it in your browser
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>LiveKit Local Client</title>
<script src="https://unpkg.com/livekit-client@latest/dist/livekit-client.umd.js"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; max-width:800px; }
label, button { display:block; margin:8px 0; }
audio { display:block; margin-top:10px; }
</style>
</head>
<body>
<h2>LiveKit Local Client</h2>
<label>
LiveKit WS URL (optional):
<input id="wsUrl" placeholder="wss://your-livekit.example.com" style="width:100%"/>
<small>Leave blank if your token encodes the server. If you get connection errors, set this to your LiveKit wss URL.</small>
</label>
<label>
Paste participant token (JWT):
<textarea id="token" rows="3" style="width:100%" placeholder="Paste a participant token here for local testing"></textarea>
</label>
<label>
Or fetch token from endpoint (optional):
<input id="tokenEndpoint" placeholder="http://localhost:5000/token/local-user/my-call-room" style="width:100%"/>
<small>If filled, clicking "Get Token & Join" will call this endpoint and use the returned token JSON { "token": "..."}.</small>
</label>
<label>
Room (for display):
<input id="roomName" placeholder="my-call-room" style="width:100%"/>
</label>
<button id="joinBtn">Get Token & Join</button>
<button id="leaveBtn" disabled>Leave</button>
<p id="status">Status: idle</p>
<div id="remoteTracks"></div>
<script>
const { connect, LocalAudioTrack } = livekitClient;
const joinBtn = document.getElementById('joinBtn');
const leaveBtn = document.getElementById('leaveBtn');
const status = document.getElementById('status');
const tokenInput = document.getElementById('token');
const tokenEndpointInput = document.getElementById('tokenEndpoint');
const wsUrlInput = document.getElementById('wsUrl');
const remoteTracksDiv = document.getElementById('remoteTracks');
const roomNameInput = document.getElementById('roomName');
let room;
function setStatus(s) {
status.textContent = 'Status: ' + s;
console.log(s);
}
async function getTokenFromEndpoint(url) {
setStatus('Fetching token from endpoint...');
const res = await fetch(url);
if (!res.ok) throw new Error('Token endpoint returned ' + res.status);
const data = await res.json();
if (!data.token) throw new Error('Token JSON must include "token" field');
return data.token;
}
joinBtn.onclick = async () => {
joinBtn.disabled = true;
leaveBtn.disabled = true;
remoteTracksDiv.innerHTML = '';
try {
let token = tokenInput.value.trim();
const endpoint = tokenEndpointInput.value.trim();
const wsUrl = wsUrlInput.value.trim();
const roomName = roomNameInput.value.trim();
if (!token && endpoint) {
token = await getTokenFromEndpoint(endpoint);
}
if (!token) {
setStatus('No token provided. Paste a token or set a token endpoint.');
joinBtn.disabled = false;
return;
}
setStatus('Connecting to LiveKit...');
// If you want to force using a specific wss URL, pass it as first arg:
// room = await connect(wsUrl || undefined, token);
// Some LiveKit deployments require explicit URL; others accept token-only.
if (wsUrl) {
room = await connect(wsUrl, token);
} else {
// Attempt connect with token only; works if token encodes the correct server info
room = await connect(undefined, token);
}
setStatus('Connected as: ' + room.localParticipant.identity + ' Room: ' + (roomName || ''));
leaveBtn.disabled = false;
// Publish microphone
try {
const ms = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
const t = ms.getAudioTracks()[0];
// Create LocalAudioTrack from MediaStreamTrack via SDK helper
const localTrack = await LocalAudioTrack.createFromMediaStreamTrack(t);
await room.localParticipant.publishTrack(localTrack);
setStatus('Published microphone');
} catch (e) {
console.warn('Microphone publish failed:', e);
setStatus('Connected (mic publish failed or blocked).');
}
// Listen for remote audio tracks and play them
room.on('trackSubscribed', (track, publication, participant) => {
if (track.kind === 'audio') {
const el = document.createElement('audio');
el.autoplay = true;
el.controls = true;
// newer SDK exposes mediaStream
if (track.mediaStream) {
el.srcObject = track.mediaStream;
} else {
// fallback: attach track to new MediaStream
const ms = new MediaStream();
ms.addTrack(track.mediaStreamTrack || track.track); // best-effort
el.srcObject = ms;
}
const label = document.createElement('div');
label.textContent = `Audio from ${participant.identity}`;
remoteTracksDiv.appendChild(label);
remoteTracksDiv.appendChild(el);
}
});
room.on('participantConnected', p => {
setStatus('Participant connected: ' + p.identity);
});
room.on('participantDisconnected', p => {
setStatus('Participant disconnected: ' + p.identity);
});
room.on('disconnected', () => {
setStatus('Disconnected');
joinBtn.disabled = false;
leaveBtn.disabled = true;
});
} catch (err) {
console.error(err);
setStatus('Error: ' + (err.message || err));
joinBtn.disabled = false;
}
};
leaveBtn.onclick = async () => {
if (room) {
room.disconnect();
room = null;
}
joinBtn.disabled = false;
leaveBtn.disabled = true;
setStatus('Left');
remoteTracksDiv.innerHTML = '';
};
</script>
</body>
</html>
How to run locally
• Option A (double-click): Save file and open in Chrome/Firefox by double-clicking. Some browsers block getUserMedia or require HTTPS for certain features — if microphone access is blocked, use Option B.
• Option B (simple local static server — recommended):
a. Python 3: python -m http.server 8000 in the folder containing client.html
b. Open http://localhost:8000/client.html
This avoids some browser restrictions when opening file:// directly.
If you need to automatically dispatch the agent into the room at join time
• Either pre-dispatch the agent from your existing Python code, or
• Add an endpoint that dispatches the agent and returns a token; then set the "token endpoint" field to that endpoint so the client triggers agent dispatch before joining.
Want me to:
• Provide a one-click example where the token endpoint both dispatches the agent and returns a token (Flask example), or
• Adjust this page to force a specific LiveKit wss URL for your deployment?broad-boots-9735
02/13/2026, 4:06 PMtall-airline-16756
02/13/2026, 4:06 PM<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>LiveKit Simple Client</title>
<script src="https://unpkg.com/livekit-client@latest/dist/livekit-client.umd.js"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; max-width:600px; }
button { margin: 8px 0; }
textarea { width:100%; }
audio { display:block; margin-top:8px; }
</style>
</head>
<body>
<h3>LiveKit Simple Client</h3>
<!-- Paste your token and optional WS URL here -->
<script>
// Paste your participant JWT token (paste here for local testing)
const TOKEN = "PASTE_YOUR_PARTICIPANT_JWT_HERE";
// Optional: set your LiveKit WebSocket URL (wss://...). Leave as "" to try token-only connect.
const WS_URL = ""; // e.g. "<wss://your-livekit-instance.com>"
</script>
<p>
Token: <small>paste into the variable at top of file</small>
</p>
<button id="joinBtn">Join Room</button>
<button id="leaveBtn" disabled>Leave</button>
<p id="status">Status: idle</p>
<div id="remote"></div>
<script>
const { connect, LocalAudioTrack } = livekitClient;
const joinBtn = document.getElementById('joinBtn');
const leaveBtn = document.getElementById('leaveBtn');
const status = document.getElementById('status');
const remote = document.getElementById('remote');
let room;
function setStatus(s) {
status.textContent = 'Status: ' + s;
console.log(s);
}
joinBtn.onclick = async () => {
if (!TOKEN || TOKEN === "PASTE_YOUR_PARTICIPANT_JWT_HERE") {
setStatus('Please paste your participant token into the TOKEN variable at the top of this file.');
return;
}
joinBtn.disabled = true;
remote.innerHTML = '';
try {
setStatus('Connecting...');
// Connect: pass WS_URL if provided, otherwise attempt token-only connect
if (WS_URL && WS_URL.trim() !== "") {
room = await connect(WS_URL, TOKEN);
} else {
room = await connect(undefined, TOKEN);
}
setStatus('Connected as ' + room.localParticipant.identity);
leaveBtn.disabled = false;
// Publish microphone (ask permission)
try {
const ms = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
const t = ms.getAudioTracks()[0];
const localTrack = await LocalAudioTrack.createFromMediaStreamTrack(t);
await room.localParticipant.publishTrack(localTrack);
setStatus('Connected — microphone published');
} catch (e) {
console.warn('Mic publish failed:', e);
setStatus('Connected — microphone not published (permission denied or error)');
}
// Play incoming audio tracks (agent or other participants)
room.on('trackSubscribed', (track, publication, participant) => {
if (track.kind === 'audio') {
const el = document.createElement('audio');
el.autoplay = true;
el.controls = true;
// SDK exposes mediaStream for audio tracks
if (track.mediaStream) {
el.srcObject = track.mediaStream;
} else {
// Fallback: create MediaStream and add available track reference
const ms = new MediaStream();
if (track.mediaStreamTrack) ms.addTrack(track.mediaStreamTrack);
el.srcObject = ms;
}
const label = document.createElement('div');
label.textContent = 'Audio from ' + participant.identity;
remote.appendChild(label);
remote.appendChild(el);
}
});
room.on('disconnected', () => {
setStatus('Disconnected');
joinBtn.disabled = false;
leaveBtn.disabled = true;
});
} catch (err) {
console.error(err);
setStatus('Connection error: ' + (err.message || err));
joinBtn.disabled = false;
}
};
leaveBtn.onclick = () => {
if (room) {
room.disconnect();
room = null;
}
setStatus('Left room');
joinBtn.disabled = false;
leaveBtn.disabled = true;
remote.innerHTML = '';
};
</script>
</body>
</html>
How to run
• Save and open in Chrome/Firefox. If microphone access is blocked when opening via file://, serve it with a simple static server:
◦ Python 3: run python -m http.server 8000 in the folder and open http://localhost:8000/client.html
Notes
• This is for local testing only. Do not embed long-lived tokens in production pages.
• If connect fails, set WS_URL to your LiveKit wss URL (e.g., "wss://your-livekit-instance.com") and try again.
• If you want, paste your token here (or tell me any error message you see) and I’ll help debug connection errors.