Skip to content

Support MoQ direct mode, reading the session from the URL - #51

Draft
kixelated wants to merge 3 commits into
pipecat-ai:mainfrom
kixelated:claude/moq-direct-mode
Draft

Support MoQ direct mode, reading the session from the URL#51
kixelated wants to merge 3 commits into
pipecat-ai:mainfrom
kixelated:claude/moq-direct-mode

Conversation

@kixelated

Copy link
Copy Markdown

What

Adds support for MoQ direct mode, where the bot waits on the relay rather than being started by a /start request. There is no rendezvous response to ask where to meet it, so the client reads the session from the page URL instead and connects the transport straight from those values.

Depends on the Pipecat runner's --moq-direct flag (pipecat-ai/pipecat#5123).

How

  • relay, ns, botId, clientId are read from the query string, as the runner prints them, and passed to MoqTransport as constructor options. Leaving startBotParams unset is what skips the POST — PipecatAppBase connects the transport directly when no endpoint is in play, so no change was needed in voice-ui-kit.
  • A URL carrying relay also opens on the MoQ transport rather than the default, since it names the session to join.
  • Each page load mints its own session id and hangs both broadcast paths off it (<client-id>/<uuid>, <bot-id>/<uuid>). Everyone sharing a direct-mode URL shares a namespace, so without this the second visitor publishes over the first and neither call works. The runner watches the request prefix and starts a bot per id it sees.
  • ns is optional and defaults to empty, putting calls at the relay root. Falling back to the transport's built-in pipecat namespace would quietly place an unscoped call on a path shared with everyone else who omitted it — on a public relay, both a collision and a disclosure.

Without relay in the URL, MoQ behaves exactly as before.

botId/clientId are passed explicitly because @pipecat-ai/moq-transport still defaults them to the older bot0/client0 pair, while the bot now publishes its broadcast as the response and reads the peer's as the request.

Testing

Ran against a live --moq-direct bot: the UI opens on MoQ from the URL, connects with no /start request (confirmed by filtering the network log), negotiates RTVI, renders the bot's greeting, and carries typed input back to the bot over the MoQ transcript track. Two browsers on one URL each get their own bot with disjoint paths. tsc --noEmit and vite build clean.

Note that a direct-mode session also needs pipecat-ai/pipecat-client-web-transports#167 — without it the client subscribes to the bot's broadcast before the bot exists and the streams are reset.

(written by Opus 5)

kixelated and others added 3 commits July 31, 2026 16:32
A bot run with `--moq-direct` is already on the relay before anyone opens
the client, so there is no `/start` to ask where to meet it. Read the
session from the page URL instead — `relay`, `ns`, `botId`, `clientId`,
as the Pipecat runner prints them — and hand those to the transport as
constructor options.

Leaving `startBotParams` unset is what skips the POST: the base connects
the transport directly when no endpoint is in play. A URL carrying
`relay` also opens on the MoQ transport, since it names the session to
join. Without it, MoQ still goes through `/start` unchanged.

`botId`/`clientId` are passed explicitly because the transport still
defaults to the older bot0/client0 pair, while the bot now publishes its
broadcast as the response and reads the peer's as the request.

Co-Authored-By: Claude <noreply@anthropic.com>
Everyone opening a direct-mode URL shares a namespace, so a fixed
participant id meant the second visitor published over the first. Mint a
random id per page load and hang both broadcast paths off it, giving
each caller their own call on a shared URL.

The runner watches the request prefix and starts a bot per id it sees.
Path.from joins components with "/", so a slashed id needs nothing from
the transport package.

Co-Authored-By: Claude <noreply@anthropic.com>
Falling back to the transport's built-in `pipecat` namespace put an
unscoped call on a path shared with everyone else who omitted `ns`,
which on a public relay is both a collision and a disclosure. Default to
empty instead, so the call sits at the relay root and `ns` is genuinely
optional; the per-page session id is what keeps callers apart.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant