Reference

Connection and Handshake

← Back to System Internals Reference

Endpoints & Credentials

Thing Value
Control socket wss://conversation.avatar.us.kaltura.ai path /socket.io
STV WHEP base srsBaseUrl from appInit
STV WHEP signaling POST {webrtc_url} if stvNewSession returned one, else POST {srsBaseUrl}/rtc/v1/whep/?app=app&stream={session_id} (body: plain SDP, Content-Type: application/sdp). SDK:wire.js whepUrl()
TURN the turnServerUrl value returned by appInit (default username/credential in wire.js's turnServers(), overridable via creds). See TURN configuration below.
Auth Socket.IO auth: { token: <enrichedKS> } + query.partnerId

All of conversationManagerUrl, srsBaseUrl, turnServerUrl, and the enriched ks come from POST https://api.avatar.us.kaltura.ai/v1/application/appInit (see Backend API Reference). The agent is identified by partnerId (from the KS) and the KS itself, not by clientId or flowId. Both of those are optional and unused by Kaltura agents.

TURN configuration

Set explicit ports and transports on the TURN address. A bare turn:host gives no relay candidate. When that happens, packetsSent stays at 0 and the avatar can't hear you. Use all four forms:

iceTransportPolicy (SDK:wire.js iceConfig()) is per-channel:

The setting matters less than it looks: the ASR server advertises only a private host candidate, so the pair relays through TURN either way, regardless of policy. This means the TURN URLs must be correct.

Full per-client matrix: Wire Protocol · Audio Channels §5.


Socket.IO Connection

import { io } from 'socket.io-client';

const socket = io(conversationManagerUrl, {   // from appInit
  path: '/socket.io',
  transports: ['websocket'],
  auth: { token: enrichedKs },                // from appInit
  query: {
    partnerId: '<your_partner_id>',           // derived from the KS
    clientId: undefined,                       // optional; unused for Kaltura agents
    flowId: undefined,                         // optional; unused for Kaltura agents
    billed_client: '',
    stickyId: '<random-16>',
    level: 'published',
    debugMode: true
  }
});

Full Connect Sequence (state-machine order)

Exact order from the platform's built-in client's connection state machine. Steps 1–5 are serial: each waits for the named inbound event before advancing. After step 5 the SDK runs two lanes in parallel: lane A is steps 6→7→9 (agent, ready, ASR uplink), lane B is step 10 (WHEP), which needs only the step 5 result. Step 11 runs once both lanes are done. The first lane to fail rejects connect() at once. Step 0 is never awaited: the mic prompt runs alongside the whole sequence and a denied mic emits a warning, never a failure. Timeouts appear in the last column.

# Client does Emits (→) / Waits (←) Inbound event Timeout
0 Init WebRTC session (TURN config) + start getUserMedia(audio:true,video:false) in the background — (browser mic prompt) — (not awaited)
1 Open socket ← onServerConnected {finalUrl, loadingVideoURL, agentName, hostName} 10s
2 Join room → join (see payload below) — —
3 Wait config + join ack ← clientConfiguration, ← joinComplete both required clientConfiguration 5s, joinComplete 20s (both JoinRoomTimeout)
4 Create STV session → stvNewSession {room_id, cast_mode} — —
5 Wait session ← stvNewSession {session_id, status, webrtc_url?} (or ← throwToNoAgent) sets sessionId + webrtcUrl —
6 Wait agent ← showAgent {} agent joined 10s
7 Wait ready ← askPermissions {constraints:{audio,video}} server ready —
8 (optional) wait player-ready, 1s delay — — —
9 Connect ASR (mic uplink), lane A, after 6→7 asr-webrtc-* handshake (below) — 30s per wait
10 Subscribe STV video (WHEP) and wait until it is playable, or give up waiting, lane B, starts right after step 5 → WHEP POST (no timeout of its own) → wait <video> canplay + ~300ms settle, or a 6s hard cap if canplay never fires first decoded frame, or the 6s cap elapsing 6s (hard cap; settles either way)
11 Approve (this starts the spoken greeting), once lanes A and B are both done → approvedPermissions {client, room} — —
→ CONNECTED listen for agent_raw_text, generatingSpeech, stvStartedTalking — —

Overall connecting timeout: 30s. It bounds every wait in the table, including the two ASR waits and the WHEP answer: an event or WHEP answer that lands after the deadline rejects connect() with ConnectTimeout.

Why step 3 has two timeouts, not one. The server emits clientConfiguration immediately on join. It emits joinComplete only after an awaited context-update call, which can take more than 5s under load. The SDK budgets the two waits separately: clientConfiguration gets 5s, joinComplete gets 20s. See Wire Protocol · Connection Basics §3 for the full rationale. Conflating them into one 5s budget causes spurious JoinRoomTimeout failures on loaded rooms.

This 30s deadline is set once, at the start of connect(). It keeps running through every step below, including the capacity queue, and is not paused or extended when the queue activates. If the account is queued (throwToNoAgent / availabilityResult{available:false}) and no slot frees up before the 30s runs out, connect() rejects with ConnectTimeout. To wait longer than that for a slot, call waitForCapacity({maxWaitMs, pollIntervalMs}) before connect(). This is a separate, opt-in poll with its own bound: maxWaitMs defaults to 300000ms. See Capacity & the queue.

Why approvedPermissions waits for playable video. Sending it too early clips the greeting: ICE connected fires about 2s before the first frame decodes, and approvedPermissions is what makes the server start speaking. _approve (SDK:session.js) gates on <video> reaching canplay/HAVE_FUTURE_DATA, with a 6s hard cap so a stalled video track can't block approval forever. See Wire Protocol · Connection Basics §3 for the full rationale. The opening line itself can't be interrupted; typed text sent during it is held (speak()) until stvFinishedTalking. For the fastest interruptible start, give the avatar a silent opening phrase (SILENT_OPENING) and let the session's kickoff option send the first turn on that event. See Start the Conversation.


The join payload (step 2): carries the agent/brain config

socket.emit('join', {
  client: clientId,            // optional
  room: roomId,                // a client-generated room id (also sent as 'channel')
  channel: roomId,
  kaltura: {
    entryId: <entryId>,            // only if context is a media entry
    contextId: <contextId>,        // category/entry the KB is scoped to
    contextType: <contextType>,    // the type of contextId (e.g. 'entry' vs 'category')
    threadId: <existingThreadId>,  // to resume a conversation thread
    force_experience: 'avatar_only',
    capabilities: {                // brain capabilities, same enum as intellect config
      avatar: 'on',
      generate_followup_questions: 'on',
      use_knowledge_base: 'off',   // forced off when an entryId is set
      // use_content_search, include_sources, etc.
    }
  },
  userAgent, userAgentHints, isMobile,
  channel_password: null, peer_name: 'unknown',
  peer_video: false, peer_audio: true
});
Doc Covers
System Internals Reference · Audio & Video Wiring ASR uplink + STV downlink
System Internals Reference · Conversation Flow What streams while connected, sending user input, the message catalog
System Internals Reference Back to the index
Click to talk with Nova — she knows this whole SDK.
Nova AI assistant — knows this whole site

Reloading starts a fresh chat. “New conversation” does the same without leaving the drawer.