Agora is a battle-tested RTC SDK for interactive live streaming and video calling — AgoraRTC.createClient({ mode: "rtc", codec: "vp8" }) creates a client for video calls; mode: "live" is for large-scale streaming. client.join(appId, channel, token, uid) connects to a channel. AgoraRTC.createMicrophoneAudioTrack() and AgoraRTC.createCameraVideoTrack() capture local media. client.publish([audioTrack, videoTrack]) sends media to the channel. client.on("user-published", (user, mediaType) => client.subscribe(user, mediaType)) handles remote users. localVideoTrack.play("element-id") renders video in a DOM element. client.leave() disconnects. createScreenVideoTrack() shares the screen. Tokens are generated server-side with RtcTokenBuilder.buildTokenWithUid(appId, appCertificate, channelName, uid, role, expireTime). agora-rtc-react provides useLocalMicrophoneTrack, useLocalCameraTrack, useRemoteUsers, and useJoin hooks. Claude Code generates Agora video rooms, live streaming stages, audio conferences, screen sharing, and cloud recording workflows.
CLAUDE.md for Agora
## Agora Stack
- Version: agora-rtc-sdk-ng >= 4.21, agora-rtc-react >= 2.4
- Client: const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" })
- Join: await client.join(AGORA_APP_ID, channelName, token, uid)
- Tracks: const [mic, cam] = await Promise.all([AgoraRTC.createMicrophoneAudioTrack(), AgoraRTC.createCameraVideoTrack()])
- Publish: await client.publish([mic, cam])
- Subscribe: client.on("user-published", async (user, type) => { await client.subscribe(user, type); user.videoTrack?.play("video-element") })
- React: useJoin({ appid, channel, token }), useLocalCameraTrack(), useRemoteUsers()
- Token: RtcTokenBuilder.buildTokenWithUid(appId, cert, channel, uid, role, expire)
Token Server
// app/api/agora/token/route.ts — Agora RTC token server
import { NextRequest, NextResponse } from "next/server"
import { RtcTokenBuilder, RtcRole } from "agora-token"
import { auth } from "@clerk/nextjs/server"
import { z } from "zod"
const TokenSchema = z.object({
channelName: z.string().min(1).max(64).regex(/^[a-zA-Z0-9_-]+$/),
uid: z.number().int().min(0).optional().default(0),
role: z.enum(["publisher", "subscriber"]).optional().default("publisher"),
})
export async function POST(request: NextRequest) {
const { userId } = await auth()
if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
const body = TokenSchema.safeParse(await request.json())
if (!body.success) return NextResponse.json({ error: "Invalid request" }, { status: 400 })
const { channelName, uid, role } = body.data
const appId = process.env.AGORA_APP_ID!
const appCertificate = process.env.AGORA_APP_CERTIFICATE!
// Token valid for 4 hours
const expireTime = Math.floor(Date.now() / 1000) + 4 * 3600
const agoraRole = role === "publisher" ? RtcRole.PUBLISHER : RtcRole.SUBSCRIBER
const token = RtcTokenBuilder.buildTokenWithUid(
appId,
appCertificate,
channelName,
uid,
agoraRole,
expireTime,
expireTime,
)
return NextResponse.json({
token,
uid,
channelName,
appId,
expireTime,
})
}
React Video Call Component
// components/video/AgoraVideoRoom.tsx — Agora real-time video call
"use client"
import {
AgoraRTCProvider,
useJoin,
useLocalMicrophoneTrack,
useLocalCameraTrack,
usePublish,
useRemoteUsers,
useRemoteVideoTracks,
useRemoteAudioTracks,
LocalVideoTrack,
RemoteVideoTrack,
} from "agora-rtc-react"
import AgoraRTC from "agora-rtc-sdk-ng"
import { useState, useEffect } from "react"
const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" })
interface VideoRoomProps {
channelName: string
onLeave: () => void
}
export function AgoraVideoRoom({ channelName, onLeave }: VideoRoomProps) {
return (
<AgoraRTCProvider client={client}>
<VideoRoomInner channelName={channelName} onLeave={onLeave} />
</AgoraRTCProvider>
)
}
function VideoRoomInner({ channelName, onLeave }: VideoRoomProps) {
const [token, setToken] = useState<string | null>(null)
const [uid, setUid] = useState<number>(0)
const [micEnabled, setMicEnabled] = useState(true)
const [cameraEnabled, setCameraEnabled] = useState(true)
// Fetch token from server
useEffect(() => {
const uidNum = Math.floor(Math.random() * 100000)
setUid(uidNum)
fetch("/api/agora/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ channelName, uid: uidNum }),
})
.then(r => r.json())
.then(data => setToken(data.token))
.catch(console.error)
}, [channelName])
const { isConnected } = useJoin({
appid: process.env.NEXT_PUBLIC_AGORA_APP_ID!,
channel: channelName,
token,
uid,
}, token !== null)
const { localMicrophoneTrack } = useLocalMicrophoneTrack(micEnabled)
const { localCameraTrack } = useLocalCameraTrack(cameraEnabled)
usePublish([localMicrophoneTrack, localCameraTrack])
const remoteUsers = useRemoteUsers()
const { videoTracks } = useRemoteVideoTracks(remoteUsers)
const { audioTracks } = useRemoteAudioTracks(remoteUsers)
// Play remote audio
audioTracks.forEach(track => track.play())
if (!isConnected) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<div className="size-10 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-3" />
<p className="text-sm text-muted-foreground">Connecting...</p>
</div>
</div>
)
}
return (
<div className="flex flex-col h-full bg-black">
{/* Video grid */}
<div className={`flex-1 grid gap-1 p-2 ${
remoteUsers.length === 0 ? "grid-cols-1" :
remoteUsers.length === 1 ? "grid-cols-2" :
remoteUsers.length <= 3 ? "grid-cols-2" :
"grid-cols-3"
}`}>
{/* Local video */}
<div className="relative rounded-lg overflow-hidden bg-slate-900">
{localCameraTrack && (
<LocalVideoTrack track={localCameraTrack} play className="w-full h-full object-cover" />
)}
{!cameraEnabled && (
<div className="absolute inset-0 flex items-center justify-center bg-slate-800">
<span className="text-4xl">👤</span>
</div>
)}
<span className="absolute bottom-2 left-2 text-white text-xs bg-black/50 px-2 py-0.5 rounded">You</span>
</div>
{/* Remote videos */}
{videoTracks.map(track => {
const user = remoteUsers.find(u => u.uid === track.getUserId())
return (
<div key={track.getUserId()} className="relative rounded-lg overflow-hidden bg-slate-900">
<RemoteVideoTrack track={track} play className="w-full h-full object-cover" />
{!user?.hasVideo && (
<div className="absolute inset-0 flex items-center justify-center bg-slate-800">
<span className="text-4xl">👤</span>
</div>
)}
<span className="absolute bottom-2 left-2 text-white text-xs bg-black/50 px-2 py-0.5 rounded">
User {track.getUserId()}
</span>
{!user?.hasAudio && (
<span className="absolute top-2 right-2 text-white text-xs bg-black/50 px-1.5 py-0.5 rounded">🔇</span>
)}
</div>
)
})}
</div>
{/* Controls */}
<div className="flex items-center justify-center gap-3 p-4">
<button
onClick={() => setMicEnabled(m => !m)}
className={`size-11 rounded-full flex items-center justify-center text-lg font-medium ${
micEnabled ? "bg-white/10 text-white hover:bg-white/20" : "bg-red-500 text-white"
}`}
title={micEnabled ? "Mute" : "Unmute"}
>
{micEnabled ? "🎤" : "🔇"}
</button>
<button
onClick={() => setCameraEnabled(c => !c)}
className={`size-11 rounded-full flex items-center justify-center text-lg ${
cameraEnabled ? "bg-white/10 text-white hover:bg-white/20" : "bg-red-500 text-white"
}`}
title={cameraEnabled ? "Stop Camera" : "Start Camera"}
>
{cameraEnabled ? "📷" : "🚫"}
</button>
<button
onClick={onLeave}
className="size-11 bg-red-600 hover:bg-red-700 rounded-full flex items-center justify-center text-lg"
title="Leave"
>
📵
</button>
<div className="ml-2 text-white/60 text-xs">
{remoteUsers.length + 1} participant{remoteUsers.length !== 0 ? "s" : ""}
</div>
</div>
</div>
)
}
Live Streaming Component
// lib/agora/liveStream.ts — broadcaster/audience live streaming setup
import AgoraRTC, { IAgoraRTCClient } from "agora-rtc-sdk-ng"
export type StreamRole = "broadcaster" | "audience"
export async function createLiveStreamClient(role: StreamRole): Promise<IAgoraRTCClient> {
const client = AgoraRTC.createClient({
mode: "live",
codec: "h264",
})
await client.setClientRole(role === "broadcaster" ? "host" : "audience")
return client
}
// Network quality monitoring
export function monitorNetworkQuality(client: IAgoraRTCClient, onChange: (quality: number) => void) {
client.on("network-quality", (stats) => {
// uplink: 0 (unknown) → 6 (very bad)
onChange(stats.uplinkNetworkQuality)
})
}
// Cloud recording utilities
export async function startCloudRecording(channelName: string, uid: number, token: string) {
// Acquire a resource
const acquireRes = await fetch(
`https://api.agora.io/v1/apps/${process.env.AGORA_APP_ID}/cloud_recording/acquire`,
{
method: "POST",
headers: {
Authorization: `Basic ${Buffer.from(`${process.env.AGORA_CUSTOMER_ID}:${process.env.AGORA_CUSTOMER_SECRET}`).toString("base64")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ cname: channelName, uid: String(uid), clientRequest: {} }),
},
)
const { resourceId } = await acquireRes.json()
// Start recording
const startRes = await fetch(
`https://api.agora.io/v1/apps/${process.env.AGORA_APP_ID}/cloud_recording/resourceid/${resourceId}/mode/mix/start`,
{
method: "POST",
headers: {
Authorization: `Basic ${Buffer.from(`${process.env.AGORA_CUSTOMER_ID}:${process.env.AGORA_CUSTOMER_SECRET}`).toString("base64")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
cname: channelName,
uid: String(uid),
clientRequest: {
token,
storageConfig: {
vendor: 1, // AWS S3
region: 0,
bucket: process.env.AWS_RECORDING_BUCKET,
accessKey: process.env.AWS_ACCESS_KEY_ID,
secretKey: process.env.AWS_SECRET_ACCESS_KEY,
},
recordingConfig: {
maxIdleTime: 30,
streamTypes: 2, // Audio + video
channelType: 0, // Communication
videoStreamType: 0,
transcodingConfig: { width: 1280, height: 720, fps: 30, bitrate: 2000 },
},
},
}),
},
)
const { sid } = await startRes.json()
return { resourceId, sid }
}
For the LiveKit alternative when an open-source WebRTC platform with self-hosting options, simpler token generation, React components via @livekit/components-react, and WebRTC data channels is preferred over Agora’s SDK-based approach — LiveKit is newer and developer-friendly while Agora has more proven performance at massive scale (millions of concurrent users) and better mobile SDK coverage, see the LiveKit guide. For the Daily.co alternative when a fully managed video API with pre-built React call UI, recording, and network quality management as a hosted service is preferred over Agora’s SDK — Daily.co trades lower-level control for a faster integration time, see the Daily.co guide. The Claude Skills 360 bundle includes Agora skill sets covering video calls, live streaming, and token servers. Start with the free tier to try RTC application generation.