Transports connect your session to the user's microphone, speakers, and camera.
attachVoiceis required for any live session.attachVideois layered on top when the session usesmode: "video".
Voice and Video Transports
Voice Transport — attachVoice
attachVoice connects the session to a LiveKit room, publishes the user's microphone, wires agent audio playback, and exposes speaking indicators and mute controls. It must be called before attachVideo.
import { createSession } from "@picoagent/embed-core";
import { attachVoice } from "@picoagent/embed-voice";
const credential = await fetch("/api/your-token-endpoint").then(r => r.json());
const session = await createSession({ credential });
const voice = attachVoice(session, {
onAgentSpeaking: (speaking) => console.log("Agent speaking:", speaking),
onMuteChange: (muted) => updateMuteButton(muted),
});
session.on("plan", (e) => renderPlan(e.plan));
session.on("step_update", (e) => updateStepUI(e.stepIndex, e.totalSteps));
session.on("session_ended", (e) => showEndScreen(e.reason));
document.getElementById("next-btn").onclick = () =>
session.send({ type: "command", cmd: "next_step" });
Speaking Indicator
The onAgentSpeaking callback fires whenever the agent starts or stops speaking. Use it to animate a speaking indicator in your UI:
const voice = attachVoice(session, {
onAgentSpeaking: (speaking) => {
document.getElementById("agent-indicator").classList.toggle("speaking", speaking);
},
});
Mute Controls
The voice object exposes three mute methods. Use them to build a mute toggle button:
// Toggle mute on button press
document.getElementById("mute-btn").onclick = () => {
if (voice.isMuted()) {
voice.unmute();
} else {
voice.mute();
}
};
The onMuteChange callback receives the new mute state whenever it changes — useful for keeping your button UI in sync with state changes that originate outside your button (e.g. a keyboard shortcut or an external control).
Video Transport — attachVideo
attachVideo publishes the user's camera to the LiveKit room and sends frames to the agent for analysis. It requires an existing voice transport — attachVoice establishes the LiveKit room and data channel that attachVideo shares.
import { createSession } from "@picoagent/embed-core";
import { attachVoice } from "@picoagent/embed-voice";
import { attachVideo } from "@picoagent/embed-video";
const session = await createSession({ credential });
const voice = attachVoice(session);
const video = attachVideo(session, { fps: 3, cameraFacing: "environment" });
await video.enableCamera();
attachVideo Options
| Option | Type | Default | Description |
|---|---|---|---|
fps | number | 3 | Frame rate for video analysis (1–10). Must match the videoFps baked into the credential. |
cameraFacing | "environment" | "user" | "environment" | "environment" requests the rear camera (typical for field use). "user" requests the front/selfie camera. |
videoProcessor | string | "none" | Processing pipeline applied to each frame. Use "yolo-pose" for pose estimation. Must match the videoProcessor baked into the credential. |
video.enableCamera() prompts the user for camera permission and begins publishing. Call it in response to a user gesture (e.g. a button click) to avoid browser autoplay restrictions.
Order Requirement
Always call attachVoice before attachVideo. attachVoice establishes the LiveKit room connection and the data channel the agent uses to receive commands. attachVideo layers on top of that same room — calling it before attachVoice will throw.
// Correct order
const voice = attachVoice(session);
const video = attachVideo(session, { fps: 3 });
// Wrong — will throw
const video = attachVideo(session, { fps: 3 }); // room not yet open
const voice = attachVoice(session);
Browser Permissions
Both attachVoice and attachVideo rely on browser media permissions. If the user denies microphone or camera access, the browser throws a NotAllowedError, which surfaces as an error event on the session.
Best practices:
- Call
attachVoice(andvideo.enableCamera()) inside a user gesture handler — this avoids browsers blocking the permission prompt. - Listen for
session.on("error", ...)and surface a clear message when permission is denied. - On iOS Safari, the camera and microphone must be requested from within a user-visible interaction. Autoplay restrictions are strict.
Cleanup
Call session.disconnect() when the user leaves the page or your component unmounts. This closes the LiveKit room cleanly and ends the session on the server.
// In a React component
useEffect(() => {
return () => {
session.disconnect();
};
}, [session]);
// Vanilla JS — on page unload
window.addEventListener("beforeunload", () => session.disconnect());
Failing to disconnect leaves the LiveKit room open until it times out, which may consume credits and delay the session being recorded as ended.
Next Steps
- Getting a Session Credential — Backend token endpoint and
createSession() - Plan State, Glanceable, Knowledge, Credits — Consuming agent events and state streams