LiveKit
WebRTC rooms on a LiveKit server or LiveKit Cloud, with LiveKitTransport, LiveKitRESTHelper and the runner's token helpers.
Overview
LiveKitTransport joins a LiveKit room as a participant. Every other participant in the room (a
browser with LiveKit's client SDK, a phone call through LiveKit SIP, another bot) can talk to the
bot. LiveKit's server routes the media, so the bot needs no public UDP ports of its own.
LiveKitTransport: audio in and out (onepipecat-audiomicrophone track out, each participant's audio in), optional video out (onepipecat-videocamera track) and video in (camera and screen share), data messages (RTVI travels over them), participant metadata, and SIP DTMF in and out.LiveKitRESTHelper: LiveKit's room API overfetch(deleteRoomByName).@pipedog/runner/livekit: token helpers (generateToken,generateTokenWithAgent,generateSessionTokens,configure) and the development runner's-t livekit.
Upstream page
Pipecat's documentation for the same transport.
Source
transport.ts, utils.ts and rtc.ts.
LiveKit docs
Servers, LiveKit Cloud, tokens and client SDKs.
Runtimes
| Runtime | Works | What it needs |
|---|---|---|
| Bun | ✅ yes | Nothing. |
| Node.js | ✅ yes | Nothing. |
| Deno | ✅ yes | Nothing. |
| Cloudflare Workers | ❌ no | Nothing. |
Generated from pipecat.runtimes and pipecat.compat in packages/transport-livekit/package.json: ✅ means the compat smoke test passes on that runtime.
| Bun | Node ≥ 22 | Deno | Cloudflare Workers | |
|---|---|---|---|---|
LiveKitTransport (media) | ✅ | ✅ | ✅ (--allow-all) | ❌ |
LiveKitRESTHelper, @pipedog/runner/livekit (tokens, room API) | ✅ | ✅ | ✅ | not verified |
The transport's media goes through LiveKit's Node SDK, @livekit/rtc-node, which loads a native
N-API addon. The end-to-end test joins a real room on Bun 1.4, Node 24 and Deno 2.9 and checks data
messages and audio both ways. Workers can't load native code, so the transport doesn't run there.
Importing the package works everywhere: the addon is only loaded when the transport is set up.
The token helpers sign with livekit-server-sdk, which is plain JavaScript on WebCrypto and
fetch.
Installation
bun add @pipedog/transport-livekit @livekit/rtc-nodeimport { LiveKitParams, LiveKitTransport } from "@pipedog/transport-livekit/transport";
import { LiveKitRESTHelper } from "@pipedog/transport-livekit/utils";
import { configure, generateSessionTokens } from "@pipedog/runner/livekit";@livekit/rtc-node is an optional peer dependency, so install it yourself (1.1.0 or newer in the
1.x line). It ships prebuilt binaries for these platforms only:
| OS | Architectures |
|---|---|
| macOS | arm64 (darwin-arm64), x64 (darwin-x64) |
| Linux with glibc | x64 (linux-x64-gnu), arm64 (linux-arm64-gnu) |
| Windows | x64 (win32-x64-msvc) |
There is no build for musl (Alpine Linux), so use a Debian or Ubuntu based image. Without the package, or on another platform, setting up the transport fails with an error that says how to install it.
Prerequisites
- A LiveKit server. Either LiveKit Cloud or your own
livekit-server(for local development:docker run --rm -p 7880:7880 -p 7881:7881 -p 7882:7882/udp livekit/livekit-server --dev --node-ip 127.0.0.1, whose key isdevkeyand secretsecret). - Its API key and secret, to sign access tokens:
LIVEKIT_API_KEYandLIVEKIT_API_SECRET. The server URL goes inLIVEKIT_URL(wss://<project>.livekit.cloud, orws://localhost:7880). - A client in the same room: LiveKit's Agents Playground, Meet, or your own app with a LiveKit client SDK.
- A long-running Bun, Node or Deno process on one of the platforms above.
LiveKitTransport
Configuration
new LiveKitTransport({ url, token, roomName, params?, inputName?, outputName? }). The token is
an access token whose identity is the bot's (see Tokens).
Prop
Type
LiveKitParams is a TransportParams with three more fields:
Prop
Type
Usage
import { Pipeline } from "@pipedog/core/pipeline/pipeline";
import { PipelineWorker } from "@pipedog/core/pipeline/worker";
import { WorkerRunner } from "@pipedog/core/workers/runner";
import { configure } from "@pipedog/runner/livekit";
import { LiveKitParams, LiveKitTransport } from "@pipedog/transport-livekit/transport";
// LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET and LIVEKIT_ROOM_NAME (or -r/--room).
// configure() mints an agent token for the bot and logs one for a user.
const [url, token, roomName] = await configure();
const transport = new LiveKitTransport({
url,
token,
roomName,
params: new LiveKitParams({ audioInEnabled: true, audioOutEnabled: true }),
});
const pipeline = new Pipeline([
transport.input(),
// ... STT, LLM context aggregators, LLM, TTS
transport.output(),
]);
const worker = new PipelineWorker(pipeline);
transport.eventHandler(
"on_first_participant_joined",
async (_t: unknown, participantId: string) => {
console.log(`${participantId} joined`);
},
);
transport.eventHandler("on_participant_disconnected", async () => {
await worker.cancel();
});
const runner = new WorkerRunner();
await runner.addWorkers(worker);
await runner.run();
Event handlers
Every participantId is the participant's LiveKit identity, the one set in its token, not its
session id.
| Event | Arguments | When |
|---|---|---|
on_connected | (transport) | The bot joined the room. |
on_disconnected | (transport) | The bot left the room, or the server disconnected it. |
on_before_disconnect | (transport) | Sync, just before the bot leaves. |
on_first_participant_joined | (transport, participantId) | The first participant joined (or was already in the room). |
on_participant_connected | (transport, participantId) | A participant joined. |
on_participant_disconnected | (transport, participantId) | A participant left. |
on_participant_left | (transport, participantId, reason) | Same, with "disconnected". |
on_client_connected, on_client_disconnected | (transport, { id }) | Aliases with the shape other transports use. |
on_audio_track_subscribed, on_audio_track_unsubscribed | (transport, participantId) | A participant's audio started or stopped. |
on_video_track_subscribed, on_video_track_unsubscribed | (transport, participantId) | A participant's video started or stopped. |
on_app_message | (transport, message, participantId) | A JSON object arrived as a data message (RTVI uses this). |
on_data_received | (transport, data, participantId) | Any data message, as bytes. participantId is null for messages a server SDK sent. |
on_dtmf_event | (transport, { tone, digit, code, participant_id }) | A SIP participant pressed a key. An InputDTMFFrame is pushed too. |
on_active_speaker_changed | (transport, { id }) | The room's loudest speaker changed. |
Methods
getParticipants(),getParticipantMetadata(participantId),setMetadata(metadata).muteParticipant(participantId)/unmuteParticipant(participantId): stop or resume receiving a participant's audio (LiveKit doesn't let one participant mute another's microphone).sendMessage(message, participantId?)/sendMessageUrgent(...): a data message to everyone or to one participant.OutputTransportMessageFrames in the pipeline go out the same way.captureParticipantVideo(participantId, framerate?, videoSource?, { onRequestOnly }).
Notes
- Audio comes in at 48 kHz from LiveKit and is resampled to the pipeline's rate. With
audioInUserTracks(the default) each participant's audio is a stream ofUserAudioRawFrames with their identity; set it tofalseto mix everyone into one stream. OutputDTMFFrameis sent as a native DTMF event (publishDtmf), one key per frame.- An
InterruptionFrameclears the audio already queued in LiveKit's audio source, so the bot stops talking at once. - Connecting is retried 3 times, 4 seconds apart.
Tokens
The bot and each user need their own token for the room, signed with the API key and secret.
@pipedog/runner/livekit signs them with livekit-server-sdk:
generateTokenWithAgent(room, identity, key, secret): the bot's token (agent: true, so LiveKit's clients show it as an agent).generateToken(room, identity, key, secret): a user's token.generateSessionTokens(room, sessionId, key, secret): both, with identities suffixed by the session id so sessions sharing a room don't evict each other.configure(): readsLIVEKIT_URL,LIVEKIT_API_KEY,LIVEKIT_API_SECRETand the room (-r/--roomorLIVEKIT_ROOM_NAME), and returns[url, botToken, roomName]. It logs a user token you can paste into a client.
All of them are async (signing uses WebCrypto).
With the development runner
bun bot.ts -t livekit serves POST /start. Each call creates a session: it mints a bot token and
a user token for LIVEKIT_ROOM_NAME (or a new pipecat-xxxxxxxx room), starts your bot() with
LiveKitRunnerArguments, and answers { url, token, sessionId } for the user. createTransport
builds the transport from those arguments:
import type { RunnerArguments } from "@pipedog/runner";
import { createTransport } from "@pipedog/runner/utils";
/** The bot the development runner starts for each session (`bun bot.ts -t livekit`). */
export async function bot(runnerArgs: RunnerArguments): Promise<void> {
const transport = await createTransport(runnerArgs, {
livekit: () => new LiveKitParams({ audioInEnabled: true, audioOutEnabled: true }),
});
// ... build the pipeline around transport.input() and transport.output()
void transport;
}See Runner.
LiveKitRESTHelper
new LiveKitRESTHelper({ apiKey, apiSecret, apiUrl, fetch? }). deleteRoomByName(roomName)
deletes a room and disconnects everyone in it. apiUrl is the server's HTTP URL
(https://<project>.livekit.cloud). fetch replaces the global one (upstream takes an aiohttp
session).
pipedog is an unofficial community TypeScript port of Pipecat. It is not affiliated with or endorsed by Daily or the Pipecat project.
Portions adapted from the Pipecat documentation, Copyright (c) 2024–2025 Daily, BSD-2-Clause (see apps/docs/LICENSE-pipecat-docs).
SmallWebRTC
Peer-to-peer WebRTC with SmallWebRTCTransport, SmallWebRTCConnection, SmallWebRTCRequestHandler and the smallWebRTCOfferHandler signalling endpoint.
WhatsApp Business voice calls with WhatsAppClient, over SmallWebRTC. Handles Meta's calling webhooks, signature checks, and accepting, rejecting and ending calls.