broad-librarian-92914
12/17/2025, 12:06 PMtall-airline-16756
12/17/2025, 12:06 PMtall-airline-16756
12/17/2025, 12:07 PMbroad-librarian-92914
12/17/2025, 12:07 PMroom.dataChannels.open('app');
room.dataChannels.on('message', (data) => {
const text = typeof data === 'string' ? data : new TextDecoder().decode(data as ArrayBuffer);
console.log('data message', text);
});tall-airline-16756
12/17/2025, 12:08 PMroom.dataChannels isn't a thing in the LiveKit JS SDK. You're trying to call .open on undefined.
Use the SDK's data API instead:
1) Send data
await room.localParticipant.publishData(
new TextEncoder().encode("your message"),
{ reliable: true } // or { reliable: false } for lossy
);
2) Receive data
room.on(RoomEvent.DataReceived, (payload: Uint8Array, participant) => {
const text = new TextDecoder().decode(payload);
console.log('data message', text, 'from', participant.identity);
});
If you want to send strings without encoding each time, I can show a small helper function. Would you like that?
Sources: Data packets | LiveKit docsbroad-librarian-92914
12/17/2025, 12:10 PMroom.dataChannels.open('app');
room.dataChannels.on('message', (data) => {
const text = typeof data === 'string' ? data : new TextDecoder().decode(data as ArrayBuffer);
console.log('data message', text);
});tall-airline-16756
12/17/2025, 12:10 PMroom.dataChannels doesn't exist on the LiveKit Room object — calling .open on it tries to read a property of undefined.
Use the Room/local participant data API instead:
1) Send data
await room.localParticipant.publishData(
new TextEncoder().encode("your message"),
{ reliable: true } // or { reliable: false } for lossy
);
2) Receive data
room.on(RoomEvent.DataReceived, (payload: Uint8Array, participant) => {
const text = new TextDecoder().decode(payload);
console.log('data message', text, 'from', participant.identity);
});
If you prefer helper functions to send/receive strings (so you don't manually encode/decode each time), tell me which SDK version you're using and I’ll provide a snippet.broad-librarian-92914
12/17/2025, 12:12 PMtall-airline-16756
12/17/2025, 12:12 PMroom.dataChannels.open — instead use localParticipant.publishData() to send and RoomEvent.DataReceived to receive.
If you’re in a Next.js client component, here’s a minimal example you can drop in:
'use client';
import { useEffect, useState } from 'react';
import { Room, RoomEvent, DataPacket_Kind } from 'livekit-client';
export default function DataChannelComponent({ url, token }: { url: string; token: string }) {
const [room, setRoom] = useState<Room | null>(null);
const [messages, setMessages] = useState<{ sender: string; text: string }[]>([]);
useEffect(() => {
const r = new Room();
let mounted = true;
(async () => {
await r.connect(url, token);
r.on(RoomEvent.DataReceived, (payload: Uint8Array, participant) => {
const text = new TextDecoder().decode(payload);
if (mounted) setMessages(prev => [...prev, { sender: participant.identity, text }]);
});
if (mounted) setRoom(r);
})();
return () => {
mounted = false;
r.disconnect();
};
}, [url, token]);
const send = async (text: string) => {
if (!room) return;
const data = new TextEncoder().encode(text);
// reliable true => reliable delivery, set false for lossy
room.localParticipant.publishData(data, { reliable: true, kind: DataPacket_Kind.RELIABLE });
};
return (
<div>
<button onClick={() => send('Hello from Next.js')}>Send</button>
<div>
{messages.map((m, i) => (
<div key={i}><strong>{m.sender}:</strong> {m.text}</div>
))}
</div>
</div>
);
}
Notes:
• Use "use client"; for Next.js client components.
• Use room.localParticipant.publishData() to send, and RoomEvent.DataReceived to receive.
• I can convert this into a reusable hook or show how to integrate with @livekit/components-react if you want. Which would you prefer? Sources: Data packets | LiveKit docs | React Core and Componentsbroad-librarian-92914
12/17/2025, 12:13 PMtall-airline-16756
12/17/2025, 12:14 PMroom.dataChannels.open).
Here’s a minimal pattern using the useRoom() hook inside a Next.js client component:
'use client';
import { useEffect, useState } from 'react';
import { useRoom } from '@livekit/components-react';
import { RoomEvent, DataPacket_Kind } from 'livekit-client';
export default function DataChannel() {
const room = useRoom();
const [messages, setMessages] = useState<{ sender: string; text: string }[]>([]);
useEffect(() => {
if (!room) return;
const onData = (payload: Uint8Array, participant) => {
const text = new TextDecoder().decode(payload);
setMessages(prev => [...prev, { sender: participant.identity, text }]);
};
room.on(RoomEvent.DataReceived, onData);
return () => room.off(RoomEvent.DataReceived, onData);
}, [room]);
const send = (text: string) => {
if (!room) return;
const data = new TextEncoder().encode(text);
room.localParticipant.publishData(data, { reliable: true, kind: DataPacket_Kind.RELIABLE });
};
return (
<div>
<button onClick={() => send('Hello from React')}>Send</button>
<div>
{messages.map((m, i) => (<div key={i}><strong>{m.sender}:</strong> {m.text}</div>))}
</div>
</div>
);
}
Notes:
• Use "use client"; for Next.js client components.
• Use useRoom() (from @livekit/components-react) to access room.
• Use room.localParticipant.publishData() to send and RoomEvent.DataReceived to receive.
• I can convert this into a reusable hook, show how to scope messages to specific participants, or provide an example using LiveKitRoom wrapper — which would you like? Sources: Data packets | LiveKit docs | React Core and Components | Web and mobile frontends | LiveKit docsbroad-librarian-92914
12/17/2025, 12:17 PMtall-airline-16756
12/17/2025, 12:17 PMuseRoom isn't exported by the version of @livekit/components-react you have installed.
Two quick paths to resolve this — which would you like to try?
• Upgrade to a version that exports useRoom. Which version of @livekit/components-react is in your package.json? I can tell you the minimum version that includes useRoom.
• Use the current API for your installed version. Tell me the package version and I’ll provide the correct import/hooks (or an alternate pattern) for sending/receiving data with that version.
If you prefer, paste your package.json dependency line for @livekit/components-react and I’ll give the exact code to use.broad-librarian-92914
12/17/2025, 12:18 PMtall-airline-16756
12/17/2025, 12:18 PMuseRoom error, please paste the @livekit/components-react version from your package.json (or the exact dependency line).
If you prefer not to share that, tell me whether you want:
• a snippet using useRoom from @livekit/components-react/hooks, or
• an alternative that uses the LiveKitRoom wrapper + `useRoomContext`/`useLiveKitRoom` (for older versions).
Which do you want me to provide?
Sources: Data packets | LiveKit docs | Handling events | LiveKit docsbroad-librarian-92914
12/17/2025, 12:21 PMLiveKitRoom wrapper + `useRoomContext`/`useLiveKitRoom`tall-airline-16756
12/17/2025, 12:21 PMLiveKitRoom wrapper and the context hook (useRoomContext / useLiveKitRoom) to access the room instance and then call room.localParticipant.publishData() to send and listen for RoomEvent.DataReceived to receive.
Minimal working example (Next.js client component):
'use client';
import { LiveKitRoom, useRoomContext } from '@livekit/components-react';
import { RoomEvent, DataPacket_Kind } from 'livekit-client';
import { useEffect, useState } from 'react';
function DataUI() {
const room = useRoomContext();
const [messages, setMessages] = useState<{ sender: string; text: string }[]>([]);
const [text, setText] = useState('');
useEffect(() => {
if (!room) return;
const onData = (payload: Uint8Array, participant) => {
const msg = new TextDecoder().decode(payload);
setMessages(prev => [...prev, { sender: participant.identity, text: msg }]);
};
room.on(RoomEvent.DataReceived, onData);
return () => room.off(RoomEvent.DataReceived, onData);
}, [room]);
const send = () => {
if (!room || !text.trim()) return;
const data = new TextEncoder().encode(text);
room.localParticipant.publishData(data, { reliable: true, kind: DataPacket_Kind.RELIABLE });
setText('');
};
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} placeholder="Message" />
<button onClick={send}>Send</button>
<div>
{messages.map((m, i) => (<div key={i}><strong>{m.sender}:</strong> {m.text}</div>))}
</div>
</div>
);
}
export default function MyRoomWrapper({ serverUrl, token }: { serverUrl: string; token: string }) {
return (
<LiveKitRoom serverUrl={serverUrl} token={token} connect={true}>
<DataUI />
{/* other components */}
</LiveKitRoom>
);
}
Notes:
• Use "use client"; in Next.js for client components.
• LiveKitRoom provides the room via useRoomContext() / useLiveKitRoom().
• Use room.localParticipant.publishData() to send and RoomEvent.DataReceived to receive.
• If your installed @livekit/components-react exports different hook names, tell me your package version and I’ll adapt the snippet. Sources: client-sdk-js | livekit/livekit_composite | DeepWiki | Expo quickstart | LiveKit docs | Data packets | LiveKit docs | client-sdk-swift | livekit/livekit_composite | DeepWikibroad-librarian-92914
12/17/2025, 12:24 PMtall-airline-16756
12/17/2025, 12:24 PMMyRoomWrapper) where you want to join the room, passing serverUrl and `token`:
◦ Example page (Next.js app router page component):
'use client';
import MyRoomWrapper from './components/MyRoomWrapper';
export default function Page() {
return <MyRoomWrapper serverUrl="wss://your-livekit" token="{TOKEN}" />;
}
1. Inside MyRoomWrapper the LiveKitRoom component handles connecting. Any child (like DataUI) can call useRoomContext() to get the room instance and send/receive data.
Best practices
• Fetch/generate the join token on the server (API route) and pass it to the client page; do not expose server keys in client code.
• Keep LiveKit connection in a single wrapper per logical room to avoid multiple connections.
• Clean up on unmount — LiveKitRoom handles disconnects for you when used as shown.
If you want, I can:
• Provide a full example showing a Next.js page that calls an API to get a token and then renders the wrapper, or
• Show how to scope the wrapper to only part of the page. Which would you like?broad-librarian-92914
12/17/2025, 12:34 PMtall-airline-16756
12/17/2025, 12:34 PMpublishData runs before the Room/RTCEngine is fully connected or after it disconnected. Here’s how to fix and debug it.
1) Ensure you only publish after the room is connected
• Use the LiveKitRoom callbacks or room state to wait for connection. Example with `LiveKitRoom`:
<LiveKitRoom serverUrl={serverUrl} token={token} connect={true} onConnected={(room) => { /* now safe to publish */ }}>
<DataUI />
</LiveKitRoom>
• Or in your component, check room is defined and connected before sending:
if (!room || room.connectionState !== 'connected') return;
await room.localParticipant.publishData(...);
2) Send from UI actions (avoid sending during mount unless after connect)
• If you auto-send on mount, subscribe to the connected event first (or call publish from an onClick).
3) Prevent race/unmount issues
• Keep the component mounted while connecting/sending.
• Remove any timers or async flows that call publishData after you unmounted or disconnected.
4) Add defensive checks and retries
• Catch the error and either retry after a short delay or show a user-friendly message:
try {
await room.localParticipant.publishData(data, { reliable: true });
} catch (err) {
// handle UnexpectedConnectionState: retry or notify
}
5) Useful debugging checks
• Log room.connectionState and room.rtc?.pcState before publish.
• Confirm LiveKitRoom connect prop is true and token/server are valid.
• Check for any explicit room.disconnect() calls or navigation that unmounts the wrapper before publish.
If you want, paste the snippet where you call publishData (and your LiveKitRoom usage) and I’ll point out the exact race/unmount cause and a minimal fix.broad-librarian-92914
12/17/2025, 12:36 PMtall-airline-16756
12/17/2025, 12:36 PMuseParticipants / useParticipant or useRoomContext() to get remote and local participants.
• Render two video elements (one for local, one for remote) using the participant’s videoTracks or the LiveKit React Video components.
Minimal pattern:
• In LiveKitRoom children, get participants:
◦ local: room.localParticipant
◦ remote: room.participants (map of remote participants)
• Render video components for each participant stream. Ensure "use client" for Next.js.
If you want code, tell me whether you use @livekit/components-react or custom rendering and your package version.
2) Capture conversation audio/text for summary Options:
• Transcription approach (recommended for accurate text summary)
◦ Send audio to a speech-to-text service (client-side streaming or server-side):
▪︎ Client-side: capture audio and stream via WebRTC/data to a transcription service (complex).
▪︎ Server-side: record or forward audio to server (via SDK or selective forwarding) and use a speech-to-text API to produce transcripts.
◦ Once you have text, run summarization (LLM or extractive) on the transcript.
• Data-channel approach (lighter)
◦ Publish interim text via room.localParticipant.publishData() (participants send typed messages or client-side speech recognition results).
◦ Collect messages server-side or in-room and summarize them with an LLM.
3) Implementation sketch (quick)
• UI: two video panes + transcript area + summary panel.
• When room is connected, subscribe to RoomEvent.DataReceived to collect messages/transcript fragments.
• If using speech-to-text on client:
◦ Use Web Speech API (browser) to get continuous transcripts, send fragments via publishData.
• If using server transcription:
◦ Stream audio to your server (or upload recordings) and transcribe there; push transcript via websocket or API to client.
• Summarization:
◦ Send full transcript to a summarization API (your LLM) and display the result. Optionally generate highlights, action items, speaker attribution.
4) Practical tips
• Attribute speaker by using participant.identity when receiving transcript fragments.
• For real-time feel, show live transcript as fragments arrive and update summary after conversation or on demand.
• Keep privacy in mind: get consent before recording/transcribing.
• If low latency is required, client-side speech recognition + incremental summarization works well.
• Use reliable publishData for transcript fragments to avoid loss.
If you want, I can:
• Provide a concrete React + LiveKit code example that renders both videos and collects data events.
• Show a sample flow that uses the browser Speech API to publish transcripts via publishData.
• Outline a server-side transcription + summarization architecture and example API endpoints.
Which of the three (code example for video+data handling, client-side speech-to-text snippet, or server-side transcription + summarization architecture) would you like next?broad-librarian-92914
12/17/2025, 12:40 PM@livekit/components-reacttall-airline-16756
12/17/2025, 12:40 PM'use client';
import React from 'react';
import { LiveKitRoom } from '@livekit/components-react';
export default function RoomWrapper({
serverUrl,
token,
children,
}: {
serverUrl: string;
token: string;
children: React.ReactNode;
}) {
return (
<LiveKitRoom serverUrl={serverUrl} token={token} connect={true}>
{children}
</LiveKitRoom>
);
}
2) Interview UI component (InterviewRoom.tsx)
'use client';
import React, { useEffect, useState, useRef } from 'react';
import {
useRoomContext,
useTracks,
Track,
useLocalParticipant,
} from '@livekit/components-react';
import { RoomEvent, DataPacket_Kind } from 'livekit-client';
function VideoTile({ track }: { track: any }) {
// `track` here is a VideoTrackPublicationRef from useTracks
return <video ref={(el) => { if (el && track.track) el.srcObject = track.track.mediaStreamTrack && new MediaStream([track.track.mediaStreamTrack]); }} autoPlay playsInline muted={track.source === Track.Source.Camera ? true : false} style={{ width: 320, height: 240 }} />;
}
export default function InterviewRoom({ onRequestSummary }: { onRequestSummary: (transcript: string) => void; }) {
const room = useRoomContext();
const localParticipant = useLocalParticipant();
const cameraTracks = useTracks([Track.Source.Camera], { onlySubscribed: false });
const [remoteCameraTracks, setRemoteCameraTracks] = useState<any[]>([]);
const [messages, setMessages] = useState<{ sender: string; text: string; ts: number }[]>([]);
const speechRef = useRef<any>(null);
useEffect(() => {
// update remote camera tracks when tracks change
setRemoteCameraTracks(cameraTracks.filter(t => t.publication?.participant !== room?.localParticipant?.sid));
}, [cameraTracks, room]);
useEffect(() => {
if (!room) return;
const onData = (payload: Uint8Array, participant) => {
try {
const text = new TextDecoder().decode(payload);
const parsed = JSON.parse(text);
if (parsed?.transcript) {
setMessages(prev => [...prev, { sender: participant.identity, text: parsed.transcript, ts: Date.now() }]);
}
} catch (e) {
// ignore non-JSON messages
}
};
room.on(RoomEvent.DataReceived, onData);
return () => room.off(RoomEvent.DataReceived, onData);
}, [room]);
// Start browser speech recognition and publish fragments via data channel
const startTranscription = async () => {
if (!('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)) {
alert('Browser does not support SpeechRecognition API');
return;
}
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
const recog = new SpeechRecognition();
recog.lang = 'en-US';
recog.interimResults = true;
recog.continuous = true;
recog.onresult = (ev: any) => {
let interim = '';
let final = '';
for (let i = ev.resultIndex; i < ev.results.length; ++i) {
const res = ev.results[i];
if (res.isFinal) final += res[0].transcript;
else interim += res[0].transcript;
}
const toSend = final || interim;
if (toSend && room && room.connectionState === 'connected') {
const payload = JSON.stringify({ transcript: toSend, interim: !final });
room.localParticipant.publishData(new TextEncoder().encode(payload), { reliable: true, kind: DataPacket_Kind.RELIABLE }).catch(() => {});
if (final) {
setMessages(prev => [...prev, { sender: localParticipant?.identity || 'me', text: toSend, ts: Date.now() }]);
}
}
};
recog.onerror = (e: any) => {
console.warn('speech error', e);
};
recog.onend = () => {
// restart if needed
speechRef.current = null;
};
recog.start();
speechRef.current = recog;
};
const stopTranscription = () => {
if (speechRef.current) {
try { speechRef.current.stop(); } catch {}
speechRef.current = null;
}
};
const sendManualMessage = (text: string) => {
if (!room || !text.trim()) return;
const payload = JSON.stringify({ transcript: text, interim: false });
room.localParticipant.publishData(new TextEncoder().encode(payload), { reliable: true, kind: DataPacket_Kind.RELIABLE }).catch(() => {});
setMessages(prev => [...prev, { sender: localParticipant?.identity || 'me', text, ts: Date.now() }]);
};
const requestSummary = () => {
// combine transcript and call parent handler (hook to server/LLM)
const transcript = messages.map(m => `${m.sender}: ${m.text}`).join('\n');
onRequestSummary(transcript);
};
return (
<div>
<div style={{ display: 'flex', gap: 12 }}>
<div>
<h4>Local</h4>
{cameraTracks.filter(t => t.publication?.participant === room?.localParticipant?.sid).map((t: any) => <VideoTile key={t.publication.sid} track={t} />)}
</div>
<div>
<h4>Remote</h4>
{remoteCameraTracks.map((t, i) => <VideoTile key={i} track={t} />)}
</div>
</div>
<div style={{ marginTop: 12 }}>
<button onClick={startTranscription}>Start Transcription</button>
<button onClick={stopTranscription}>Stop</button>
<button onClick={() => sendManualMessage(prompt('Message to send') || '')}>Send Manual Message</button>
<button onClick={requestSummary}>Request Summary</button>
</div>
<div style={{ marginTop: 12 }}>
<h4>Transcript</h4>
<div style={{ maxHeight: 240, overflow: 'auto', border: '1px solid #ddd', padding: 8 }}>
{messages.map((m, i) => <div key={i}><strong>{m.sender}:</strong> {m.text}</div>)}
</div>
</div>
</div>
);
}
3) Page glue that uses RoomWrapper + InterviewRoom and handles summarization (page or component)
'use client';
import React, { useState } from 'react';
import RoomWrapper from './RoomWrapper';
import InterviewRoom from './InterviewRoom';
export default function InterviewPage({ serverUrl, token }: { serverUrl: string; token: string; }) {
const [summary, setSummary] = useState<string | null>(null);
const handleRequestSummary = async (transcript: string) => {
// Replace this with your summarization API call (server-side LLM)
// For demo, we'll just take first 200 chars
// CALL YOUR SERVER: POST /api/summarize { transcript }
const fakeSummary = transcript.slice(0, 300) + (transcript.length > 300 ? '…' : '');
setSummary(fakeSummary);
};
return (
<RoomWrapper serverUrl={serverUrl} token={token}>
<InterviewRoom onRequestSummary={handleRequestSummary} />
<div style={{ marginTop: 12 }}>
<h4>Summary</h4>
<div style={{ border: '1px solid #ddd', padding: 8, minHeight: 80 }}>{summary || 'No summary yet'}</div>
</div>
</RoomWrapper>
);
}
Implementation notes and best practices
• Permissions & tokens: Generate join tokens server-side; do not embed server API keys in client code.
• SpeechRecognition: Browser API availability varies (Chrome supports webkitSpeechRecognition). For production, server-side transcription (or a robust client library) is recommended.
• Attribution: Messages include participant.identity from RoomEvent.DataReceived so you can label speakers.
• Reliability: Use { reliable: true } for transcripts so fragments arrive reliably.
• Connection state: Only publish when room.connectionState === 'connected'. The code checks that implicitly by catching publish errors; you can add explicit checks before publish.
• Summarization: Call your server endpoint that runs an LLM (OpenAI or other) with the combined transcript and return the summary. Keep that on the server to protect API keys and for cost control.
• Cleanup: LiveKitRoom handles disconnect on unmount. Ensure components are client components ('use client').
If you want, I can:
• Convert the summarization handler to a sample Next.js API route that calls an LLM.
• Swap client-side speech recognition for server-side streaming transcription (outline + sample).
• Improve video rendering using the built-in Video components from @livekit/components-react if you tell me your components version.
Which next step would you like?
Sources: Data packets | LiveKit docs | Sending text | LiveKit docsbroad-librarian-92914
12/17/2025, 12:42 PMtall-airline-16756
12/17/2025, 12:42 PMlocalParticipant.publishData() for low-latency captions or debugging.
2) Agent Backend: run a LiveKit Agent session that:
• Uses google.realtime.RealtimeModel (Gemini Live) as the llm/voice.
• Joins the same room (or is injected server-side) with audio/video input enabled so the model receives candidate audio (optionally video).
• Uses built-in turn detection so the model listens and responds naturally.
• Emits real-time voice responses back into the room (agent speaks to participants).
Example (conceptual)
• Python AgentSession snippet:
session = AgentSession( llm=google.realtime.RealtimeModel( model="gemini-2.5-flash-native-audio-preview-09-2025", voice="Puck", instructions="You are conducting a technical interview..." ), stt="assemblyai/universal-streaming" # optional STT for transcript control ) await session.start(agent=MyAgent(), room=ctx.room, room_options=RoomOptions(video_input=True))
3) Transcript capture & summarization:
• The Agent can collect or produce transcripts (Gemini Live supports STT/turn detection).
• After or during the interview, send the transcript to a Gemini LLM endpoint (non-realtime model) to produce structured summaries, scores, or action items.
• Keep summarization server-side to protect API keys.
4) Data flow options
• Real-time AI interviewer: LiveKit Agent uses Gemini Live for turn-based speech interaction and speaks directly in the room.
• Client-side captions + server summarization: Client publishes speech fragments; server aggregates and sends to Gemini LLM for summaries.
• Hybrid: Agent does the real-time conversation; clients send periodic transcript snippets for separate analytics.
Security & infra notes
• Store Gemini/Google credentials server-side; never expose in the client.
• Monitor model usage and set session limits to control costs.
• Ensure user consent for recording/transcription.
Next concrete help I can provide
• A minimal Node.js or Python Agent example showing how to instantiate RealtimeModel and join a LiveKit room.
• A Next.js API route example that calls Gemini LLM for summarization of a transcript.
• Sample message formats for sending/receiving transcripts between frontend ↔️ LiveKit Agent.
Which of those would you like next?
Sources: Gemini Live API plugin | LiveKit docs | React quickstart | LiveKit docs | Vision | LiveKit Docs | Google Gemini LLM plugin guide | LiveKit Docs | HIPAA Eligible Products and Services | LiveKitbroad-librarian-92914
12/17/2025, 2:59 PMtall-airline-16756
12/17/2025, 2:59 PMbroad-librarian-92914
12/17/2025, 3:00 PMtall-airline-16756
12/17/2025, 3:00 PMbroad-librarian-92914
12/17/2025, 3:00 PMtall-airline-16756
12/17/2025, 3:01 PMbroad-librarian-92914
12/17/2025, 3:03 PMtall-airline-16756
12/17/2025, 3:03 PMbroad-librarian-92914
12/17/2025, 3:04 PMtall-airline-16756
12/17/2025, 3:05 PMbroad-librarian-92914
12/17/2025, 3:06 PM