Claude Code for Agora: Ultra-Low Latency RTC and Live Streaming — Claude Skills 360 Blog
Blog / Backend / Claude Code for Agora: Ultra-Low Latency RTC and Live Streaming
Backend

Claude Code for Agora: Ultra-Low Latency RTC and Live Streaming

Published: May 16, 2027
Read time: 7 min read
By: Claude Skills 360

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.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free