Skip to content

Web (Next.js)

The Tele part of a web integration is one fetch. Media runs on the livekit-client SDK, shown below.

Install

npm install livekit-client@2.22.4 @livekit/components-react@2.9.24 @livekit/components-styles

Flow

  1. Your page gets a Tele user token from your backend (Authentication).
  2. It calls the Tele join endpoint.
  3. It connects a media Room with the returned ws_url, token and ice_servers.

Example

app/call/[code]/CallView.tsx
"use client";

import { useEffect, useMemo, useState } from "react";
import { Room } from "livekit-client";
import { RoomContext, VideoConference, RoomAudioRenderer, StartAudio } from "@livekit/components-react";
import "@livekit/components-styles";

const TELE_API = process.env.NEXT_PUBLIC_TELE_API!; // https://tele-api.cloudguys.io

async function teleJoin(code: string) {
  const { token } = await fetch("/api/tele-token", { method: "POST" }).then((r) => r.json()); // your backend
  const res = await fetch(`${TELE_API}/v1/invites/${encodeURIComponent(code)}/join`, {
    method: "POST",
    headers: { Authorization: `Bearer ${token}`, "X-Client-Platform": "web" },
  });
  const body = await res.json();
  if (!res.ok) throw body.error; // { code, message, opens_at? }, see Errors
  return body as { ws_url: string; token: string; ice_servers: RTCIceServer[] | null; role: string };
}

export default function CallView({ code }: { code: string }) {
  const room = useMemo(() => new Room({ adaptiveStream: true, dynacast: true }), []);
  const [error, setError] = useState<{ code: string; message: string }>();

  useEffect(() => {
    let cancelled = false;
    teleJoin(code)
      .then(async (join) => {
        if (cancelled) return;
        await room.connect(join.ws_url, join.token, {
          // Tele's TURN servers replace the SDK defaults; null → let the SDK use its own.
          rtcConfig: join.ice_servers ? { iceServers: join.ice_servers } : undefined,
        });
        await room.localParticipant.enableCameraAndMicrophone();
      })
      .catch(setError);
    return () => {
      cancelled = true;
      room.disconnect();
    };
  }, [room, code]);

  if (error) return <p>Can't join: {error.message}</p>;
  return (
    <RoomContext.Provider value={room}>
      <VideoConference />
      <RoomAudioRenderer />
      <StartAudio label="Click to enable audio" />
    </RoomContext.Provider>
  );
}
app/call/[code]/page.tsx
import dynamic from "next/dynamic";
const CallView = dynamic(() => import("./CallView"), { ssr: false });

export default async function Page({ params }: { params: Promise<{ code: string }> }) {
  return <CallView code={(await params).code} />;
}

Gotchas

  • Client-only: keep everything media-related in "use client" components and load the call view with ssr: false.
  • HTTPS: browsers allow camera and microphone only on HTTPS (and localhost).
  • Safari autoplay: render <StartAudio/>, or call room.startAudio() after a user gesture.
  • React StrictMode: in development, effects mount twice. The cancelled flag above prevents a double connect; useSequentialRoomConnectDisconnect from @livekit/components-react is an alternative.
  • Reconnect: on RoomEvent.Disconnected with a reason other than a deliberate leave, call teleJoin again with a fresh token. See Reconnects.
  • Chat: see Chat. VideoConference includes a chat panel that uses lk.chat.