Skip to main content
This guide walks you through integrating the CometChat Calls SDK into a Next.js application. By the end, you’ll have a working video call implementation with proper server-side rendering handling and authentication.

Prerequisites

Before you begin, ensure you have:
  • A CometChat account with an app created (Sign up)
  • Your App ID, Region, and API Key from the CometChat Dashboard
  • A Next.js project (App Router or Pages Router)
  • Node.js 16+ installed

Important: Client-Side Only

The CometChat Calls SDK uses browser APIs (WebRTC, DOM) that are not available during server-side rendering. You must ensure the SDK only loads and runs on the client side. This guide shows you how to handle this properly with both the App Router and Pages Router.

Step 1: Install the SDK

Install the CometChat Calls SDK package:
npm install @cometchat/calls-sdk-javascript

Step 2: Configure Environment Variables

Add your CometChat credentials to .env.local. The NEXT_PUBLIC_ prefix makes these variables available in the browser:
NEXT_PUBLIC_COMETCHAT_APP_ID=your_app_id
NEXT_PUBLIC_COMETCHAT_REGION=us
NEXT_PUBLIC_COMETCHAT_API_KEY=your_api_key
Never expose your API Key in production client-side code. Use a backend service to generate auth tokens securely. The API Key approach shown here is for development and testing only.

Step 3: Create the Provider

Create a context provider that handles SDK initialization and user authentication. The "use client" directive ensures this component only runs in the browser.
// providers/CometChatCallsProvider.tsx
"use client";

import { createContext, useContext, useEffect, useState, ReactNode } from "react";

interface User {
  uid: string;
  name: string;
  avatar?: string;
}

interface CometChatCallsContextType {
  isReady: boolean;
  user: User | null;
  error: string | null;
  CometChatCalls: any;
}

const CometChatCallsContext = createContext<CometChatCallsContextType>({
  isReady: false,
  user: null,
  error: null,
  CometChatCalls: null,
});

interface ProviderProps {
  children: ReactNode;
  uid: string;
}

export function CometChatCallsProvider({ children, uid }: ProviderProps) {
  const [isReady, setIsReady] = useState(false);
  const [user, setUser] = useState<User | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [CometChatCalls, setCometChatCalls] = useState<any>(null);

  useEffect(() => {
    async function initAndLogin() {
      try {
        // Dynamic import ensures the SDK only loads on the client
        const { CometChatCalls: SDK } = await import("@cometchat/calls-sdk-javascript");

        // Step 1: Initialize the SDK
        const initResult = await SDK.init({
          appId: process.env.NEXT_PUBLIC_COMETCHAT_APP_ID!,
          region: process.env.NEXT_PUBLIC_COMETCHAT_REGION!,
        });

        if (!initResult.success) {
          throw new Error("SDK initialization failed");
        }

        // Step 2: Check if already logged in
        let loggedInUser = SDK.getLoggedInUser();

        // Step 3: Login if not already logged in
        if (!loggedInUser) {
          loggedInUser = await SDK.login(
            uid,
            process.env.NEXT_PUBLIC_COMETCHAT_API_KEY!
          );
        }

        setCometChatCalls(SDK);
        setUser(loggedInUser);
        setIsReady(true);
      } catch (err: any) {
        console.error("CometChat Calls setup failed:", err);
        setError(err.message || "Setup failed");
      }
    }

    if (uid) {
      initAndLogin();
    }
  }, [uid]);

  return (
    <CometChatCallsContext.Provider value={{ isReady, user, error, CometChatCalls }}>
      {children}
    </CometChatCallsContext.Provider>
  );
}

export function useCometChatCalls(): CometChatCallsContextType {
  return useContext(CometChatCallsContext);
}

Step 4: Add Provider to Layout (App Router)

Wrap your application with the provider. Since the provider is a client component, you can still use it in a server component layout:
// app/layout.tsx
import { CometChatCallsProvider } from "@/providers/CometChatCallsProvider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  // In a real app, get this from your authentication system
  const currentUserId = "cometchat-uid-1";

  return (
    <html lang="en">
      <body>
        <CometChatCallsProvider uid={currentUserId}>
          {children}
        </CometChatCallsProvider>
      </body>
    </html>
  );
}

Step 5: Create the Call Component

Build a client-side call component that handles joining sessions, media controls, and cleanup. The component uses the SDK instance from the context provider:
// components/CallScreen.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import { useCometChatCalls } from "@/providers/CometChatCallsProvider";

interface CallScreenProps {
  sessionId: string;
  onCallEnd?: () => void;
}

export default function CallScreen({ sessionId, onCallEnd }: CallScreenProps) {
  const { isReady, CometChatCalls } = useCometChatCalls();
  const containerRef = useRef<HTMLDivElement>(null);
  
  // Call state
  const [isJoined, setIsJoined] = useState(false);
  const [isJoining, setIsJoining] = useState(false);
  const [isMuted, setIsMuted] = useState(false);
  const [isVideoOff, setIsVideoOff] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  // Store unsubscribe functions for cleanup
  const unsubscribersRef = useRef<Function[]>([]);

  useEffect(() => {
    // Don't proceed if SDK isn't ready or container isn't mounted
    if (!isReady || !CometChatCalls || !containerRef.current || !sessionId) return;

    async function joinCall() {
      setIsJoining(true);
      setError(null);

      try {
        // Register event listeners before joining
        unsubscribersRef.current = [
          CometChatCalls.addEventListener("onSessionJoined", () => {
            setIsJoined(true);
            setIsJoining(false);
          }),
          CometChatCalls.addEventListener("onSessionLeft", () => {
            setIsJoined(false);
            onCallEnd?.();
          }),
          CometChatCalls.addEventListener("onAudioMuted", () => setIsMuted(true)),
          CometChatCalls.addEventListener("onAudioUnMuted", () => setIsMuted(false)),
          CometChatCalls.addEventListener("onVideoPaused", () => setIsVideoOff(true)),
          CometChatCalls.addEventListener("onVideoResumed", () => setIsVideoOff(false)),
        ];

        // Generate a call token for this session
        const tokenResult = await CometChatCalls.generateToken(sessionId);

        // Join the call session
        const joinResult = await CometChatCalls.joinSession(
          tokenResult.token,
          {
            sessionType: "VIDEO",
            layout: "TILE",
            startAudioMuted: false,
            startVideoPaused: false,
          },
          containerRef.current
        );

        if (joinResult.error) {
          throw new Error(joinResult.error.message);
        }
      } catch (err: any) {
        console.error("Failed to join call:", err);
        setError(err.message || "Failed to join call");
        setIsJoining(false);
      }
    }

    joinCall();

    // Cleanup when component unmounts
    return () => {
      unsubscribersRef.current.forEach((unsub) => unsub());
      unsubscribersRef.current = [];
      CometChatCalls?.leaveSession();
    };
  }, [isReady, CometChatCalls, sessionId, onCallEnd]);

  // Control handlers
  const toggleAudio = () => {
    isMuted ? CometChatCalls.unMuteAudio() : CometChatCalls.muteAudio();
  };

  const toggleVideo = () => {
    isVideoOff ? CometChatCalls.resumeVideo() : CometChatCalls.pauseVideo();
  };

  const leaveCall = () => {
    CometChatCalls.leaveSession();
  };

  // Loading state
  if (!isReady) {
    return <div className="p-4 text-center">Initializing...</div>;
  }

  // Error state
  if (error) {
    return (
      <div className="p-4 text-center">
        <p className="text-red-500 mb-4">Error: {error}</p>
        <button 
          onClick={() => window.location.reload()}
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          Retry
        </button>
      </div>
    );
  }

  return (
    <div className="call-screen">
      {/* Video container - SDK renders the call UI here */}
      <div 
        ref={containerRef} 
        className="w-full h-[500px] bg-gray-900"
      />

      {/* Loading overlay */}
      {isJoining && (
        <div className="p-4 text-center text-gray-500">Joining call...</div>
      )}

      {/* Call controls */}
      {isJoined && (
        <div className="flex gap-2 p-4 justify-center">
          <button 
            onClick={toggleAudio}
            className={`px-6 py-3 rounded-lg text-white ${isMuted ? "bg-red-500" : "bg-green-500"}`}
          >
            {isMuted ? "Unmute" : "Mute"}
          </button>
          <button 
            onClick={toggleVideo}
            className={`px-6 py-3 rounded-lg text-white ${isVideoOff ? "bg-red-500" : "bg-green-500"}`}
          >
            {isVideoOff ? "Start Video" : "Stop Video"}
          </button>
          <button 
            onClick={leaveCall}
            className="px-6 py-3 bg-red-500 text-white rounded-lg"
          >
            Leave Call
          </button>
        </div>
      )}
    </div>
  );
}

Step 6: Create the Call Page

Create a page that uses the call component. This example shows a simple interface where users can enter a session ID and join a call:
// app/page.tsx
"use client";

import { useState } from "react";
import { useCometChatCalls } from "@/providers/CometChatCallsProvider";
import CallScreen from "@/components/CallScreen";

export default function HomePage() {
  const { isReady, user, error } = useCometChatCalls();
  const [sessionId, setSessionId] = useState("");
  const [isInCall, setIsInCall] = useState(false);

  if (error) {
    return (
      <div className="p-8 text-center">
        <p className="text-red-500">Error: {error}</p>
      </div>
    );
  }

  if (!isReady) {
    return (
      <div className="p-8 text-center">
        <p>Loading...</p>
      </div>
    );
  }

  if (isInCall) {
    return (
      <CallScreen 
        sessionId={sessionId} 
        onCallEnd={() => setIsInCall(false)} 
      />
    );
  }

  return (
    <div className="p-8 max-w-md mx-auto">
      <h1 className="text-2xl font-bold mb-4">CometChat Calls</h1>
      <p className="text-gray-600 mb-6">Logged in as: {user?.name || user?.uid}</p>
      
      <div className="space-y-4">
        <input
          type="text"
          placeholder="Enter Session ID"
          value={sessionId}
          onChange={(e) => setSessionId(e.target.value)}
          className="w-full p-3 border rounded-lg"
        />
        <button
          onClick={() => setIsInCall(true)}
          disabled={!sessionId}
          className="w-full p-3 bg-purple-600 text-white rounded-lg disabled:opacity-50"
        >
          Join Call
        </button>
      </div>
    </div>
  );
}

Dynamic Route for Call Sessions

For a cleaner URL structure, create a dynamic route that accepts the session ID as a parameter:
// app/call/[sessionId]/page.tsx
"use client";

import { useParams, useRouter } from "next/navigation";
import CallScreen from "@/components/CallScreen";

export default function CallPage() {
  const params = useParams();
  const router = useRouter();
  const sessionId = params.sessionId as string;

  return (
    <CallScreen 
      sessionId={sessionId} 
      onCallEnd={() => router.push("/")} 
    />
  );
}

Pages Router Setup

If you’re using the Pages Router instead of the App Router, use dynamic imports to prevent server-side rendering of the SDK:
// pages/_app.tsx
import type { AppProps } from "next/app";
import dynamic from "next/dynamic";

const CometChatCallsProvider = dynamic(
  () => import("@/providers/CometChatCallsProvider").then(mod => mod.CometChatCallsProvider),
  { ssr: false }
);

export default function App({ Component, pageProps }: AppProps) {
  const currentUserId = "cometchat-uid-1";

  return (
    <CometChatCallsProvider uid={currentUserId}>
      <Component {...pageProps} />
    </CometChatCallsProvider>
  );
}
// pages/call/[sessionId].tsx
import dynamic from "next/dynamic";
import { useRouter } from "next/router";

const CallScreen = dynamic(() => import("@/components/CallScreen"), {
  ssr: false,
  loading: () => <div className="p-4 text-center">Loading call...</div>,
});

export default function CallPage() {
  const router = useRouter();
  const { sessionId } = router.query;

  if (!sessionId) {
    return <div className="p-4 text-center">Loading...</div>;
  }

  return (
    <CallScreen 
      sessionId={sessionId as string} 
      onCallEnd={() => router.push("/")}
    />
  );
}

Custom Hook (Optional)

For more complex applications, extract call logic into a reusable hook:
// hooks/useCall.ts
"use client";

import { useState, useCallback, useRef, useEffect } from "react";
import { useCometChatCalls } from "@/providers/CometChatCallsProvider";

export function useCall() {
  const { CometChatCalls, isReady } = useCometChatCalls();
  const [isInCall, setIsInCall] = useState(false);
  const [isMuted, setIsMuted] = useState(false);
  const [isVideoOff, setIsVideoOff] = useState(false);
  const [participants, setParticipants] = useState<any[]>([]);
  const unsubscribersRef = useRef<Function[]>([]);

  const joinCall = useCallback(async (sessionId: string, container: HTMLElement, settings = {}) => {
    if (!CometChatCalls) return;

    // Setup listeners
    unsubscribersRef.current = [
      CometChatCalls.addEventListener("onAudioMuted", () => setIsMuted(true)),
      CometChatCalls.addEventListener("onAudioUnMuted", () => setIsMuted(false)),
      CometChatCalls.addEventListener("onVideoPaused", () => setIsVideoOff(true)),
      CometChatCalls.addEventListener("onVideoResumed", () => setIsVideoOff(false)),
      CometChatCalls.addEventListener("onParticipantListChanged", setParticipants),
      CometChatCalls.addEventListener("onSessionLeft", () => setIsInCall(false)),
    ];

    const tokenResult = await CometChatCalls.generateToken(sessionId);
    await CometChatCalls.joinSession(
      tokenResult.token,
      { sessionType: "VIDEO", layout: "TILE", ...settings },
      container
    );
    setIsInCall(true);
  }, [CometChatCalls]);

  const leaveCall = useCallback(() => {
    CometChatCalls?.leaveSession();
    unsubscribersRef.current.forEach((unsub) => unsub());
    unsubscribersRef.current = [];
    setIsInCall(false);
  }, [CometChatCalls]);

  const toggleAudio = useCallback(() => {
    isMuted ? CometChatCalls?.unMuteAudio() : CometChatCalls?.muteAudio();
  }, [CometChatCalls, isMuted]);

  const toggleVideo = useCallback(() => {
    isVideoOff ? CometChatCalls?.resumeVideo() : CometChatCalls?.pauseVideo();
  }, [CometChatCalls, isVideoOff]);

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      unsubscribersRef.current.forEach((unsub) => unsub());
    };
  }, []);

  return {
    isReady,
    isInCall,
    isMuted,
    isVideoOff,
    participants,
    joinCall,
    leaveCall,
    toggleAudio,
    toggleVideo,
  };
}
For more detailed information on specific topics covered in this guide, refer to the main documentation: