Skip to content

Implement LiveKit for Real-Time Communication

Overview: Purpose and Problem Solving

  • Our project requires stable realTimeVideo and realTimeAudio features to support collaboration between users.
  • Building a custom webrtcServer is complex and time-consuming, while LiveKit provides tested sdkTools and serverInfra ready to use.

Proposal

  • Use LiveKit as the communication platform for videoCall, audioCall, and screenShare.
  • Connect frontend with livekitClient and backend with token generation endpoint getAccessToken.

Weighting / Pros and Cons

Pros

  • LiveKit is scalable and supports adaptiveStream and dynacast, which reduce bandwidth usage.
  • Provides React components like GridLayout, ParticipantTile, and ControlBar to build UI faster.
  • Security is built-in using signed accessToken with roomName and participantIdentity.
  • Reduces development time compared to building and maintaining our own signaling system.

Cons

  • Adds dependency on a third-party service which increases monthly cost.
  • The team needs time to learn LiveKit sdk and understand realTime media flows.
  • Internet connection quality of users can affect performance, which we must handle in UI/UX.

Implementation Steps

1. Setup backend token service

import "dotenv/config";
import { createContext } from "./lib/context";
import { appRouter } from "./routers/index";
import cors from "cors";
import express from "express";
import { RoomServiceClient, AccessToken } from "livekit-server-sdk";
 
import "dotenv/config";
 
const app = express();
 
app.use(
  cors({
    origin: process.env.CORS_ORIGIN || "",
    methods: ["GET", "POST", "OPTIONS"],
  })
);
 
app.use(express.json());
 
app.get("/", (_req, res) => {
  res.status(200).send("OK");
});
 
app.get("/get-token", async (req, res) => {
  const roomName = "quickstart-room";
  const participantName = String(Date.now());
 
  const at = new AccessToken(
    process.env.LIVEKIT_API_KEY,
    process.env.LIVEKIT_API_SECRET,
    {
      identity: participantName,
      ttl: "10m",
    }
  );
  at.addGrant({
    roomJoin: true,
    room: roomName,
  });
 
  const token = await at.toJwt();
  res.send(token);
});
 
const svc = new RoomServiceClient(
  process.env.LIVEKIT_HOST as string,
  process.env.LIVEKIT_API_KEY,
  process.env.LIVEKIT_API_SECRET
);
 
app.get("/rooms", async (req, res) => {
  const rooms = await svc.listRooms();
  res.json({ rooms });
});
 
app.get("/rooms/create", async (req, res) => {
  const opts = {
    name: "first room",
    emptyTimeout: 10 * 60,
    maxParticipants: 20,
  };
 
  const room = await svc.createRoom(opts);
  console.log("room created", room);
 
  res.json({ room });
});
 
const port = process.env.PORT || 3000;
 
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

2. Connect frontend to LiveKit

import { createFileRoute } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
import {
  ControlBar,
  GridLayout,
  ParticipantTile,
  RoomAudioRenderer,
  useTracks,
  RoomContext,
} from "@livekit/components-react";
import { Room, RoomEvent, Track } from "livekit-client";
import "@livekit/components-styles";
 
export const Route = createFileRoute("/")({
  component: HomeComponent,
});
 
function HomeComponent() {
  const [room] = useState(
    () =>
      new Room({
        adaptiveStream: true,
        dynacast: true,
      }),
  );
 
  useEffect(() => {
    let mounted = true;
 
    const connect = async () => {
      const token = await fetch("http://localhost:3000/get-token").then((r) =>
        r.text(),
      );
 
      if (mounted) {
        await room.connect('wss://quochuydev-i4asls1o.livekit.cloud', token);
      }
    };
 
    connect();
 
    room.on("participantConnected", (p) => {
      console.log("Participant joined:", p.identity);
    });
 
    room.on("trackSubscribed", (track, pub, participant) => {
      console.log("Subscribed to:", track.kind, "from", participant.identity);
    });
 
    room.on(RoomEvent.Disconnected, () => {
      console.log("Left room");
    });
 
    return () => {
      mounted = false;
      room.disconnect();
    };
  }, [room]);
 
  return (
    <RoomContext.Provider value={room}>
      <div data-lk-theme="default" style={{ height: "100vh" }}>
        <MyVideoConference />
        <RoomAudioRenderer />
        <ControlBar />
      </div>
    </RoomContext.Provider>
  );
}
 
function MyVideoConference() {
  const tracks = useTracks(
    [
      {
        source: Track.Source.Camera,
        withPlaceholder: true,
      },
      {
        source: Track.Source.ScreenShare,
        withPlaceholder: false,
      },
    ],
    {
      onlySubscribed: false,
    },
  );
 
  return (
    <GridLayout
      tracks={tracks}
      style={{ height: "calc(100vh - var(--lk-control-bar-height))" }}
    >
      <ParticipantTile />
    </GridLayout>
  );
}

Conclusion

  • LiveKit gives us a proven platform to handle video, audio, and data communication without building complex webrtc logic ourselves.
  • It saves time, improves scalability, and allows the team to focus on core product features.
  • I recommend moving forward with LiveKit integration to achieve reliable real-time collaboration.