From a7dd538418af42822bd15635f3ab02afb592910a Mon Sep 17 00:00:00 2001 From: Jitvar Patil Date: Tue, 20 Oct 2026 21:29:46 +0530 Subject: [PATCH 1/2] docs(calls/javascript): ringing with the Calls SDK 5.1 Rewrite the JavaScript Ringing page for the new ringing flow (ENG-39907): the Calls SDK creates, prepares, joins and ends calls, and an incoming call still reaches an open tab through the Chat SDK's CallListener (the web has no VoIP push). Covers the caller listening on the prepared session, the callee preparing with connect(), decline/cancel/busy/ unanswered with endCall(), the status poll for declines the caller is not told about, ring timers, multiple tabs and group calls. Add Migrating from Legacy Ringing, and update setup, overview, events, actions, session settings (isPeerCall), the v5 migration guide (5.0 to 5.1), troubleshooting, and the Chat SDK calling pages. Co-Authored-By: Claude Opus 5.5 --- calls/javascript/actions.mdx | 29 +- calls/javascript/events.mdx | 40 +- .../migrating-from-legacy-ringing.mdx | 226 ++++++ calls/javascript/migration-guide-v5.mdx | 32 + calls/javascript/overview.mdx | 12 +- calls/javascript/ringing.mdx | 717 ++++++++++++------ calls/javascript/session-settings.mdx | 15 +- calls/javascript/setup.mdx | 12 +- calls/javascript/troubleshooting.mdx | 17 + docs.json | 3 +- sdk/javascript/all-real-time-listeners.mdx | 4 + sdk/javascript/calling-overview.mdx | 8 +- 12 files changed, 869 insertions(+), 246 deletions(-) create mode 100644 calls/javascript/migrating-from-legacy-ringing.mdx diff --git a/calls/javascript/actions.mdx b/calls/javascript/actions.mdx index 38ac1267e..2de4e31bf 100644 --- a/calls/javascript/actions.mdx +++ b/calls/javascript/actions.mdx @@ -5,7 +5,7 @@ sdk_version: "5.x" description: "Use CometChat Calls SDK v5 actions on JavaScript to control calls, manage audio/video state, share screens, and handle in-call behavior." --- -Use call actions to create your own custom controls or trigger call functionality dynamically based on your use case. All actions are called on the `CometChatCalls` class during an active call session. +Use call actions to create your own custom controls or trigger call functionality dynamically based on your use case. All actions are called on the `CometChatCalls` class during an active call session. The session object that `CometChatCalls.initiateCall`, `connect` and `getActiveSession()` return has every action as a method too (`session.muteAudio()`, `session.setLayout(...)`, and so on); on the session object an action called while no session is live is ignored with a console warning. ## Audio Controls @@ -202,16 +202,39 @@ CometChatCalls.lowerHand(); ### Leave Session -Ends your participation and disconnects gracefully. The call continues for other participants. +Ends your participation and disconnects gracefully. In a 1:1 call this is the hang-up: the other side receives `onParticipantLeft`, and a session joined as a peer call (`isPeerCall: true`) ends for both. In a group call the session continues for the other participants. Leaving also cancels a session that is only prepared: `onConnectionClosed` fires instead of `onSessionLeft`. A prepared peer session is already in the room, so leaving it ends the room for everyone in it, as a hang-up would. ```javascript CometChatCalls.leaveSession(); + +// Or on the session object: +await CometChatCalls.getActiveSession()?.leave(); ``` - `leaveSession()` takes no session ID. `CometChatCalls` is a singleton that tracks one active session at a time, so this leaves whichever session is currently in progress. The same applies to every action on this page. See [Single Active Session](/calls/javascript/overview#single-active-session). + + +To end a call that is still ringing, use `CometChatCalls.endCall(sessionId, status)` instead: `"cancelled"` on the caller, `"rejected"` or `"busy"` on the callee, as described in [Ringing](/calls/javascript/ringing). `leaveSession()` alone does not tell the other side why the call ended. +### Session State + +`session.isSessionActive()` returns `true` while the SDK owns a session: one being prepared or prepared (for example an outgoing call that is still ringing), one being joined, or a joined one. `session.isSessionJoined()` is the narrower "in a call" check: `true` only once the session is joined. `CometChatCalls.getSessionState()` returns `"idle"`, `"preparing"`, `"prepared"`, `"joining"` or `"joined"`. + +```javascript +const session = CometChatCalls.getActiveSession(); // null when nothing is live +const isInCall = session?.isSessionJoined() ?? false; +const state = CometChatCalls.getSessionState(); +``` + +### Get Participants + +Returns a snapshot of the remote participants of the session, the same list `onParticipantListChanged` receives. The local user is not in it. + +```javascript +const participants = CometChatCalls.getParticipants(); +``` + ## UI Controls ### Show Settings Dialog diff --git a/calls/javascript/events.mdx b/calls/javascript/events.mdx index 254f3f697..9f5074a64 100644 --- a/calls/javascript/events.mdx +++ b/calls/javascript/events.mdx @@ -20,12 +20,34 @@ const unsubscribe = CometChatCalls.addEventListener("eventName", (data) => { unsubscribe(); ``` +Listeners are global: they are not tied to a session, work before any session exists, and stay registered across sessions until you call the function they return. The session object has the same method, `session.addEventListener(eventName, listener)`, with the same behavior. To ask whether there is a session right now, use `CometChatCalls.getActiveSession()`: + +```javascript +// null when no session is preparing, prepared or joined +const session = CometChatCalls.getActiveSession(); +if (session) { + console.log("Live session", session.sessionId, session.prepareStatus); +} +``` + +`getActiveSession()` returns the session as soon as it starts **preparing**, before anyone joins: from the moment `CometChatCalls.initiateCall` has created the call on the caller, and from the moment `CometChatCalls.connect` is called on a ringing callee. It returns `null` again once the session ends, its prepare fails or is cancelled, or a prepared session expires after 5 minutes. While a session is only prepared, the SDK reports `onConnected`, `onConnectionFailed` (fatal errors only), `onConnectionClosed` and the roster events (`onParticipantConnected`, `onParticipantJoined`, `onParticipantLeft`, `onParticipantListChanged`); that is what lets a caller see "ringing" and "answered" without joining (see [Ringing](/calls/javascript/ringing)). + --- ## Session Events Monitor the call session lifecycle including join/leave events and connection status. +### Connected + +Fired when a prepared session has connected: signalling only, no camera or microphone, and other participants do not see you yet. It fires once per prepare (`initiateCall` on the caller, `connect` on the callee); the actual join is `onSessionJoined`. + +```javascript +CometChatCalls.addEventListener("onConnected", () => { + console.log("Session prepared: the join will be near-instant"); +}); +``` + ### Session Joined Fired when you successfully connect to the session. @@ -48,7 +70,7 @@ CometChatCalls.addEventListener("onSessionLeft", () => { ### Session Timed Out -Fired when the session ends due to inactivity timeout. +Fired when the session ends due to inactivity timeout. It is not used on a prepared session: one that nobody joins within 5 minutes reports `onConnectionClosed` instead. ```javascript CometChatCalls.addEventListener("onSessionTimedOut", () => { @@ -78,7 +100,7 @@ CometChatCalls.addEventListener("onConnectionRestored", () => { ### Connection Closed -Fired when the connection is permanently closed. +Fired when the connection is permanently closed. On a session that was only prepared (an outgoing call still ringing, or an incoming call this tab prepared), it means the call ended before anyone joined: declined, cancelled (in a 1:1 call), the room went away, `leave()` or `endCall` tore it down, or nobody joined within 5 minutes. The event carries no reason. ```javascript CometChatCalls.addEventListener("onConnectionClosed", () => { @@ -92,9 +114,19 @@ CometChatCalls.addEventListener("onConnectionClosed", () => { Monitor participant activities including join/leave, audio/video state, hand raise, screen sharing, and recording. +### Participant Connected + +Fired when a remote participant's device has prepared the session without joining it. On the caller's prepared session, this is the callee's device ringing. + +```javascript +CometChatCalls.addEventListener("onParticipantConnected", (participant) => { + console.log("Participant waiting:", participant.uid); +}); +``` + ### Participant Joined -Fired when a participant joins the call. +Fired when a participant joins the call. On the caller's prepared session, this is the callee answering: join with `session.join(container)` now. ```javascript CometChatCalls.addEventListener("onParticipantJoined", (participant) => { @@ -386,7 +418,7 @@ Intercept UI button clicks from the default call interface to add custom behavio ### Leave Session Button Clicked -Fired when the leave button is clicked. +Fired when the leave button is clicked. The SDK leaves the session itself right after, so only clean up here. ```javascript CometChatCalls.addEventListener("onLeaveSessionButtonClicked", () => { diff --git a/calls/javascript/migrating-from-legacy-ringing.mdx b/calls/javascript/migrating-from-legacy-ringing.mdx new file mode 100644 index 000000000..bf1586943 --- /dev/null +++ b/calls/javascript/migrating-from-legacy-ringing.mdx @@ -0,0 +1,226 @@ +--- +title: "Migrating from Legacy Ringing" +sidebarTitle: "Migrating from Legacy Ringing" +sdk_version: "5.x" +description: "Move a web app from Chat SDK call signalling (CometChat.initiateCall, acceptCall, rejectCall) to ringing with the Calls SDK 5.1: what to add, what to delete, and how each method maps." +--- + +Legacy ringing signalled calls through the Chat SDK: `CometChat.initiateCall` created the call, a `CallListener` on the Chat SDK's real-time connection told the callee, and `acceptCall`, `rejectCall` and `endCall` answered, declined and ended it. Ringing now runs on the Calls SDK: it creates the call, prepares and joins the session, and ends the ring. The Chat SDK's calling methods are superseded by the Calls SDK and no longer used. On the web, the Chat SDK's `CallListener` stays: it is still how an incoming call reaches an open tab. + +This page is the diff. For the full description of the current flow see [Ringing](/calls/javascript/ringing). + +## Before you start + +Enable the **ringing migration** setting for your app in the CometChat dashboard, then follow the +steps below. This page is for apps that use the SDKs directly. If you use the CometChat UI Kit, its +support for this flow comes with a coming UI Kit release; keep legacy ringing until then. + +{/* TODO(RC): add the setting's exact name and dashboard path and a screenshot once the toggle ships. */} + +## Why it changed + +| Legacy | Now | +|--------|-----| +| The Chat SDK created the call, and `acceptCall` / `rejectCall` / `endCall` were HTTP calls your app had to sequence with `generateToken` and `joinSession` | The Calls SDK creates the call and prepares its session. Answer = join; decline, busy, cancel and no answer are one `endCall(sessionId, status)` | +| The caller only knew "accepted" or "rejected" | The caller sees "Ringing…" (the callee's device was reached) and "answered" from the Calls SDK. A decline, busy or no answer is read from the call's status while ringing | +| Joining took a token request and a full connect after the answer | The session is prepared while it rings, so the join is near-instant | +| Web and mobile rang through different paths | One call, rung in the callee's open tabs (Chat SDK) and on the callee's phones (Push Notifications SDK) | + +## Before and after + +```mermaid +sequenceDiagram + participant A as Caller + participant Chat as Chat SDK + participant B as Callee + + rect rgb(240,240,240) + Note over A,B: Legacy ringing + A->>Chat: CometChat.initiateCall(call) + Chat-->>B: CallListener.onIncomingCallReceived + B->>Chat: CometChat.acceptCall(sessionId) + Chat-->>A: onOutgoingCallAccepted + A->>A: generateToken + joinSession + B->>B: generateToken + joinSession + A->>Chat: CometChat.endCall(sessionId) + Chat-->>B: onCallEndedMessageReceived + end +``` + +```mermaid +sequenceDiagram + participant A as Caller · Calls SDK + participant BE as Backend + participant Chat as Callee · Chat SDK + participant C as Callee · Calls SDK + + rect rgb(232,244,236) + Note over A,C: Ringing now + A->>BE: CometChatCalls.initiateCall(receiver, options) + BE-->>Chat: call message (initiated) + Chat-->>C: onIncomingCallReceived → CometChatCalls.connect(sessionId) + C-->>A: onParticipantConnected ("Ringing…") + C->>C: Answer → session.join(container) + C-->>A: onParticipantJoined → data.session.join(container) + A->>A: session.leave() to hang up + end +``` + +## Step 1: Update dependencies + +```bash +npm install @cometchat/calls-sdk-javascript@5.1.0 +# @cometchat/chat-sdk-javascript stays: chat, and incoming calls on the web +``` + +Keep both SDKs initialized and logged in as the same user: the Calls SDK for `initiateCall`, `connect` and `endCall`, the Chat SDK for the incoming call message. + +## Step 2: Rewrite the caller + +| Legacy | Now | Notes | +|--------|-----|-------| +| `CometChat.initiateCall(new CometChat.Call(receiverId, callType, receiverType))` | `CometChatCalls.initiateCall(receiver, { type, receiverType, sessionSettings })` → `{ data, error }` | `data.session` is the caller's session, already preparing. Set `sessionType` in `sessionSettings`, and `isPeerCall: true` for a 1:1 call | +| `CometChat.initiateCall(call, timeout)` | No timeout parameter | Run your own timer on the caller and end the call with `endCall(sessionId, "unanswered")` (the Connect web app uses 60 s) | +| `onOutgoingCallAccepted(call)` | `CometChatCalls.addEventListener("onParticipantJoined", ...)` on the prepared session, then `data.session.join(container)` | New in-between event: `onParticipantConnected` = the callee's device is ringing | +| `onOutgoingCallRejected(call)` | Read the call's status every 3 s while ringing; `onConnectionClosed` when the prepared session closes | Not reliably reported: a busy decline, or a decline before the callee's device connected, closes nothing. `onOutgoingCallRejected` may still arrive; keep it as an extra signal, and check `getCallInitiator()`: a 1:1 decline reaches the callee's other tabs too | +| `CometChat.rejectCall(sessionId, CometChat.CALL_STATUS.CANCELLED)` | `CometChatCalls.endCall(sessionId, "cancelled")` | Tears the caller's prepared session down first | +| `generateToken` + `joinSession` after the answer | `data.session.join(container)` | Takes no settings: the ones passed to `initiateCall` apply | + + + +```javascript +const call = new CometChat.Call(receiverId, CometChat.CALL_TYPE.VIDEO, CometChat.RECEIVER_TYPE.USER); +const outgoingCall = await CometChat.initiateCall(call); + +CometChat.addCallListener("caller", new CometChat.CallListener({ + onOutgoingCallAccepted: async (accepted) => { + const { token } = await CometChatCalls.generateToken(accepted.getSessionId()); + await CometChatCalls.joinSession(token, { sessionType: "VIDEO" }, container); + }, + onOutgoingCallRejected: () => closeOutgoingScreen(), +})); + +function cancel() { + CometChat.rejectCall(outgoingCall.getSessionId(), CometChat.CALL_STATUS.CANCELLED); +} +``` + + +```javascript +const { data: call, error } = await CometChatCalls.initiateCall(receiverId, { + type: "video", + sessionSettings: { sessionType: "VIDEO", isPeerCall: true }, +}); +if (error) throw new Error(`Could not start the call: ${error.message}`); + +const offConnected = CometChatCalls.addEventListener("onParticipantConnected", () => setStatus("Ringing…")); +const offJoined = CometChatCalls.addEventListener("onParticipantJoined", () => { + stopWatching(); + call.session.join(container); +}); +const offClosed = CometChatCalls.addEventListener("onConnectionClosed", () => closeOutgoingScreen()); +const stopWatching = () => { offConnected(); offJoined(); offClosed(); }; +// Plus: poll the call's status every 3 s and a 60 s ring timer (see Ringing). + +function cancel() { + stopWatching(); + CometChatCalls.endCall(call.sessionId, "cancelled"); +} +``` + + + +## Step 3: Rewrite the callee + +| Legacy | Now | Notes | +|--------|-----|-------| +| `CallListener.onIncomingCallReceived(call)` | Unchanged: the call still arrives on the Chat SDK's `CallListener`. Then prepare it with `CometChatCalls.connect(call.getSessionId(), settings)` | The caller's own tabs receive their 1:1 call as "incoming" too: skip calls whose `getCallInitiator()` is the logged-in user | +| `CometChat.acceptCall(sessionId)` then `generateToken` + `joinSession` | `session.join(container)` on the session `connect` prepared. Nothing else to send | If the prepare failed, `generateToken` + `joinSession` as before | +| `CometChat.rejectCall(sessionId, CometChat.CALL_STATUS.REJECTED)` | `CometChatCalls.endCall(sessionId, "rejected")` | `"busy"` when the user is in another call. A group call's decline is local: `session.leave()` | +| `onIncomingCallCancelled(call)` | Still fires (also when the caller gives up as `unanswered`); also `onConnectionClosed` on a prepared 1:1 session, and the call's status read while ringing | The status is the backstop: a cancel does not always reach a ringing tab as a message | +| — | Your own ring timer on the callee | The Connect web app stops ringing after 65 s | + + + +```javascript +CometChat.addCallListener("callee", new CometChat.CallListener({ + onIncomingCallReceived: (call) => showIncomingScreen(call), + onIncomingCallCancelled: () => closeIncomingScreen(), +})); + +async function answer(call) { + await CometChat.acceptCall(call.getSessionId()); + const { token } = await CometChatCalls.generateToken(call.getSessionId()); + await CometChatCalls.joinSession(token, { sessionType: "VIDEO" }, container); +} + +function decline(call) { + CometChat.rejectCall(call.getSessionId(), CometChat.CALL_STATUS.REJECTED); +} +``` + + +```javascript +// Settings for an incoming 1:1 call: the type comes from the call, or an audio call opens the camera. +const settingsFor = (call) => ({ + sessionType: call.getType() === "audio" ? "VOICE" : "VIDEO", + isPeerCall: true, +}); + +CometChat.addCallListener("callee", new CometChat.CallListener({ + onIncomingCallReceived: (call) => { + if (call.getCallInitiator()?.getUid() === CometChatCalls.getLoggedInUser()?.uid) return; + showIncomingScreen(call); + CometChatCalls.connect(call.getSessionId(), settingsFor(call)) + .catch(() => {}); // answer falls back to a direct join + }, + onIncomingCallCancelled: () => closeIncomingScreen(), +})); + +async function answer(call) { + const session = CometChatCalls.getActiveSession(); + if (session?.sessionId === call.getSessionId()) { + await session.join(container); + } else { + const { token } = await CometChatCalls.generateToken(call.getSessionId()); + await CometChatCalls.joinSession(token, settingsFor(call), container); + } +} + +function decline(call) { + CometChatCalls.endCall(call.getSessionId(), "rejected"); +} +``` + + + +## Step 4: Replace the remaining calls + +| Legacy | Now | Notes | +|--------|-----|-------| +| `CometChat.endCall(sessionId)` after a call | `session.leave()` or `CometChatCalls.leaveSession()` | No status to send; with `isPeerCall` the call ends for both | +| `CometChat.getActiveCall()` | `CometChatCalls.getActiveSession()` | `null` when no session is preparing, prepared or joined. `session.sessionId` names the call for a session from `initiateCall` or `connect` (it is `null` after a direct `joinSession`) | +| `CometChat.clearActiveCall()` | Nothing | The Calls SDK clears its session when it ends | +| `onCallEndedMessageReceived(call)` | `onSessionLeft` / `onConnectionClosed` on the joined session | | +| `CometChat.startCall(...)`, Chat SDK `CallSettings` | `joinSession` + the 5.x events (see the [v5 migration guide](/calls/javascript/migration-guide-v5)) | | +| `CometChat.CALL_STATUS.*` values for a decline or cancel | `"rejected"`, `"busy"`, `"cancelled"`, `"unanswered"` strings passed to `endCall` | The `Call` message model and `CometChat.CALL_STATUS` still describe call messages in a conversation's history | + +## What users will notice + +- The caller sees "Ringing…" once the callee's tab or phone is reached, and the call screen closes when the call is declined, busy or missed (from the call's status, or the ring timer). +- Answering joins almost at once, because the session was prepared while it rang. +- Calls ring between web and mobile users in both directions. +- The browser asks for the camera and microphone when the call is joined, not while it rings. + +## Checklist + +- [ ] `@cometchat/calls-sdk-javascript` 5.1; the Chat SDK stays, initialized and logged in as the same user +- [ ] **Ringing migration** enabled for the app in the dashboard +- [ ] `initiateCall` passes `sessionSettings` with `sessionType`, and `isPeerCall: true` for a 1:1 call +- [ ] Caller listens for `onParticipantConnected` / `onParticipantJoined` / `onConnectionClosed` and joins only on `onParticipantJoined` with `data.session.join(container)` +- [ ] Caller polls the call's status every 3 s while ringing and runs a ring timer that ends the call as `unanswered` +- [ ] Callee skips its own outgoing calls, prepares with `connect`, and answers with `session.join(container)` +- [ ] Callee stops ringing on `onIncomingCallCancelled`, `onConnectionClosed`, its ring timer and the status poll +- [ ] Decline, busy and cancel use `CometChatCalls.endCall`; no `CometChat.acceptCall`, `rejectCall` or `endCall` left +- [ ] Hang-up uses `session.leave()` or `CometChatCalls.leaveSession()` +- [ ] Tested: web to web, web to phone and phone to web; decline, busy, cancel, no answer; two open tabs diff --git a/calls/javascript/migration-guide-v5.mdx b/calls/javascript/migration-guide-v5.mdx index eaf686912..129e8de96 100644 --- a/calls/javascript/migration-guide-v5.mdx +++ b/calls/javascript/migration-guide-v5.mdx @@ -345,3 +345,35 @@ CometChatCalls.addEventListener("onCallLayoutChanged", (layoutType) => { }); | `CometChatCalls.OngoingCallListener` | `CometChatCalls.addEventListener()` | | `CometChatCalls.startSession()` | `CometChatCalls.joinSession()` | | `CometChatCalls.endSession()` | `CometChatCalls.leaveSession()` | + +--- + +## 5.0 to 5.1 + +5.1 moves ringing onto the Calls SDK. No 5.0 method is deprecated; the Chat SDK's calling methods are superseded: + +| Superseded | Replacement | +|------------|-------------| +| Chat SDK calling: `CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `startCall`, `getActiveCall`, `clearActiveCall`, Chat SDK `CallSettings` | `CometChatCalls.initiateCall`, `connect`, `session.join(container)`, `endCall(sessionId, status)`, `session.leave()`, `getActiveSession()` | + +`CometChat.addCallListener` is not superseded on the web: an incoming call still reaches an open tab through it. + +New in 5.1: + +- `CometChatCalls.initiateCall(receiver, options)` with `InitiateCallOptions`, resolving with `{ data, error }` where `data` is an `InitiatedCall` +- `CometChatCalls.endCall(sessionId, status)` with `EndCallStatus` (`"rejected"`, `"busy"`, `"cancelled"`, `"unanswered"`) +- `CometChatCalls.connect(sessionId, sessionSettings)`, which prepares a session ahead of the join, and `PrepareStatus` (`"none"`, `"pending"`, `"prepared"`, `"cancelled"`, `"failed"`) +- `CometChatCalls.getActiveSession()`, `CometChatCalls.getSessionState()` and `CometChatCalls.getParticipants()` +- The `CallSession` session object: `sessionId`, `callToken`, `settings`, `prepareStatus`, `settled`, `error`, `join(container)`, `leave()`, `endSessionForAll()`, `isSessionActive()`, `isSessionJoined()`, `getSessionState()`, `getParticipants()`, `addEventListener()` and every session action as a method +- The `onConnected` and `onParticipantConnected` events +- The exported `CallParticipant` type + +Changed in 5.1: + +- `CallSession` replaces the placeholder class 5.0 exported under that name: its `getInstance()` is gone, and `addEventListener()` now returns an unsubscribe function instead of `{ remove }`. Get the session from `initiateCall`, `connect` or `getActiveSession()`. +- `joinSession()` can return more errors: `SESSION_SETTINGS_REQUIRED` (no settings passed), `SESSION_ALREADY_PREPARED` and `SESSION_CANCELLED` (the join was cancelled by `leaveSession()` before it completed). A failed token verification is now returned as `error` too. +- `leaveSession()` returns a Promise that resolves when the leave has been handled. + +`joinSession()` returns `SESSION_ALREADY_PREPARED` while a session is prepared: join that one with `session.join(container)`, or cancel it with `session.leave()` first. A prepared session keeps the settings it was prepared with. + +The step-by-step move from Chat SDK signalling is on [Migrating from Legacy Ringing](/calls/javascript/migrating-from-legacy-ringing). diff --git a/calls/javascript/overview.mdx b/calls/javascript/overview.mdx index 6d460c87f..c034fdf70 100644 --- a/calls/javascript/overview.mdx +++ b/calls/javascript/overview.mdx @@ -93,10 +93,19 @@ sequenceDiagram App->>CometChatCalls: leaveSession() ``` +Every call is a session joined by ID. Where the ID comes from depends on the kind of call: + +- **Direct call**: your app shares a session ID (for example in a message) and each participant generates a token and calls `joinSession` with it. Group calls and meetings work this way. +- **Ringing call**: the caller starts it with `CometChatCalls.initiateCall`, the callee's open tabs receive it through the Chat SDK and prepare it with `CometChatCalls.connect`, and both sides join the prepared session with `session.join(container)` once the callee answers. See [Ringing](/calls/javascript/ringing). + ## Features + + Start a call, ring the callee's open tabs and phones, show "Ringing…", answer, decline, cancel and end + + Tile, Sidebar, and Spotlight view modes for different call scenarios @@ -140,6 +149,7 @@ The SDK is organized around these core components: | `CometChatCalls` | Main entry point for SDK initialization, authentication, session management, and call actions | | `CallAppSettings` | Configuration object for SDK initialization (App ID, Region) | | `CallSettings` | Configuration object for individual call sessions | +| `CallSession` | The session object `initiateCall`, `connect` and `getActiveSession()` return: `sessionId`, `prepareStatus`, `join(container)`, `leave()` and every session action | | `addEventListener` | Method to register event listeners for session, participant, media, and UI events | ## Single Active Session @@ -156,7 +166,7 @@ CometChatCalls.leaveSession(); | Behavior | Detail | |----------|--------| -| One session at a time | The SDK does not support being connected to two sessions simultaneously. Leave the current session before joining another. | +| One session at a time | The SDK does not support being connected to two sessions simultaneously. Leave the current session before joining another. A session that is only prepared (an outgoing call still ringing) counts too: `joinSession` refuses to run while it exists. | | Session methods take no session ID | `leaveSession()` and the other [actions](/calls/javascript/actions) apply to the active session only. | | Actions apply to the active session | `muteAudio()`, `pauseVideo()`, `startScreenShare()`, and similar methods target the session currently in progress. | | Events are not session-scoped | Listeners registered with `addEventListener` fire for the active session. | diff --git a/calls/javascript/ringing.mdx b/calls/javascript/ringing.mdx index ecde63523..716d3653e 100644 --- a/calls/javascript/ringing.mdx +++ b/calls/javascript/ringing.mdx @@ -2,331 +2,584 @@ title: "Ringing" sidebarTitle: "Ringing" sdk_version: "5.x" -description: "Configure CometChat Calls SDK v5 ringing on JavaScript for incoming calls, outgoing calls, call alerts, and accept or reject flows." +description: "Ring users from a web app with the CometChat Calls SDK: start a call, show it ringing, answer, decline, cancel and end it, and detect a decline the caller is not told about." --- -Implement incoming and outgoing call notifications by integrating the Calls SDK with the CometChat Chat SDK for call signaling. +Ringing lets one user call another and lets the callee answer or decline. On the web, an incoming call rings in every open tab of your app where the callee is logged in. + +Two SDKs do the work: + +| SDK | Owns | +|-----|------| +| **Calls SDK** (`@cometchat/calls-sdk-javascript` 5.1+) | Creating the call, preparing and joining the session, declining, cancelling, ending, and the call events your code listens to | +| **Chat SDK** (`@cometchat/chat-sdk-javascript`) | Delivering an incoming call to an open tab: the call message arrives on the Chat SDK's real-time connection and is reported to `CometChat.addCallListener` | + +The web has no VoIP push. A tab that is closed does not ring, and your app plays the ringtone, shows the incoming-call screen and stops ringing when the ring is over. A callee on Android or iOS rings through the Push Notifications SDK as usual, so web and mobile users can call each other. -The Calls SDK handles the actual call session. For call signaling (ringing, accept, reject), use the CometChat Chat SDK's calling features. +**Coming from the earlier ringing flow?** The Chat SDK's calling methods (`CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `startCall`) are superseded by the Calls SDK and are no longer used. `CometChat.addCallListener` is the exception on the web: it is still how an incoming call reaches a tab. See [Migrating from legacy ringing](/calls/javascript/migrating-from-legacy-ringing) for a method-by-method mapping. -## Overview +## How ringing works + +The caller creates the call with the Calls SDK. The CometChat backend delivers the call message to the callee's open tabs through the Chat SDK, and to the callee's phones by push. The callee's tab rings and prepares the session while it rings; when the callee answers, it joins. The caller's app learns that the callee is ringing and has answered from the Calls SDK, without joining the session first. + +```mermaid +sequenceDiagram + participant A as Caller app + participant BE as CometChat backend + participant Chat as Callee · Chat SDK + participant C as Callee · Calls SDK + + A->>BE: CometChatCalls.initiateCall(receiver, options) + Note over A: Session prepared, not joined ("Calling…") + BE-->>Chat: call message (initiated) + Chat->>Chat: onIncomingCallReceived → your app rings + Chat->>C: CometChatCalls.connect(sessionId, settings) + C-->>A: onParticipantConnected → "Ringing…" + C->>C: Answer → session.join(container) + C-->>A: onParticipantJoined → caller joins too + Note over A,C: In call. Hang up with session.leave() +``` -Call flow with ringing: +What each side sees: -1. **Initiator** sends a call request via Chat SDK -2. **Receiver** gets notified of incoming call -3. **Receiver** accepts or rejects the call -4. **Both parties** join the call session using Calls SDK +| Stage | Caller | Callee (web) | +|-------|--------|--------| +| Call created | `initiateCall` resolves; show "Calling…" | `onIncomingCallReceived` in each open tab; ring and call `connect` | +| Callee's device reached | `onParticipantConnected` → show "Ringing…" | Ringing | +| Callee answers | `onParticipantJoined` → `data.session.join(container)` | `session.join(container)` | +| Callee declines / is busy / does not answer | Not reliably reported. [Read the call's status](#detect-a-decline-busy-or-no-answer) every few seconds while ringing, and keep your [ring timer](#ring-timeout) | `endCall(sessionId, "rejected")` or `"busy"`; your own ring timer | +| Caller cancels | — (you ended it) | `onIncomingCallCancelled`, or (1:1) `onConnectionClosed` on the prepared session; [poll the status](#stop-ringing-on-the-callee) as a backstop | +| Either side hangs up | `onParticipantLeft` / `onSessionLeft` | same | + + +The caller is not reliably told when, or why, the callee ended the ring. A decline closes the caller's prepared session (`onConnectionClosed`) only when the callee's device had prepared it. A busy decline never does (the callee is in another call and never joins the room), and neither does a decline made before the callee's device connected. A `CallListener.onOutgoingCallRejected` call message may arrive on the Chat SDK, but not in every case. And if the caller's own prepare failed, the caller is not in the room at all and hears neither the ring nor the answer. So while the call rings, read its status from the server every few seconds and end the screen on `rejected`, `busy` or any other end, as the [ring timer](#ring-timeout) does when nothing arrives. + ## Prerequisites -Install both SDKs: +- [Calls SDK set up](/calls/javascript/setup) and the user [logged in](/calls/javascript/authentication) with `CometChatCalls.login` or `loginWithAuthToken`. `initiateCall`, `connect` and `endCall` need a logged-in Calls SDK user. +- The Chat SDK initialized and logged in as the **same user**, so that incoming calls reach the tab. See [Chat SDK setup](/sdk/javascript/setup-sdk). +- HTTPS in production, for camera and microphone access. ```bash -npm install @cometchat/chat-sdk-javascript @cometchat/calls-sdk-javascript +npm install @cometchat/calls-sdk-javascript@5.1.0 @cometchat/chat-sdk-javascript ``` -## Initialize Both SDKs +## Start a call + +`CometChatCalls.initiateCall` creates the call on the backend, which rings the callee, and starts preparing the caller's session. The caller does **not** join yet. ```javascript -import { CometChat } from "@cometchat/chat-sdk-javascript"; import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; -const appId = "APP_ID"; -const region = "REGION"; +const isGroup = false; // true to ring a group (see Group calls) +const sessionSettings = { + sessionType: "VOICE", // "VIDEO" for a video call + isPeerCall: !isGroup, // 1:1: the call ends for both when either leaves +}; + +const { data: call, error } = await CometChatCalls.initiateCall(receiverUid, { + type: "audio", // or "video" (the default) + receiverType: isGroup ? "group" : "user", + sessionSettings, +}); + +if (error) { + console.error("Could not start the call:", error.name, error.message); +} else { + console.log("Calling", call.receiver, "session", call.sessionId); + showOutgoingScreen(call); +} +``` -// Initialize Chat SDK -await CometChat.init(appId, new CometChat.AppSettingsBuilder() - .subscribePresenceForAllUsers() - .setRegion(region) - .build() -); +| Parameter | Description | +|-----------|-------------| +| `receiver` | UID of the user to call, or the group's GUID with `receiverType: "group"` (see [Group calls](#group-calls)) | +| `options.type` | `"audio"` or `"video"` (default) | +| `options.receiverType` | `"user"` (default) or `"group"` | +| `options.sessionSettings` | The [session settings](/calls/javascript/session-settings) the caller's session is prepared with. See the warning below | -// Initialize Calls SDK -await CometChatCalls.init({ appId, region }); +`initiateCall` never rejects. It resolves with `{ data, error }`: -// Login to both -await CometChat.login(uid, apiKey); -await CometChatCalls.login(uid, apiKey); -``` +- `data` is the `InitiatedCall`: `sessionId`, `messageId` (the id of the call message in the conversation), `conversationId`, `sender`, `receiver`, `receiverType`, `type`, `status`, `initiatedAt` (in seconds) and `session`, the `CallSession` being prepared. +- `error` is set when no call was created: `USER_NOT_LOGGED_IN`, `VALIDATION_ERROR` (no receiver), `SESSION_ALREADY_ACTIVE` (the user is joining or in another call; leave it first), `BAD_RESPONSE`, or the failure of the request. -## Initiate a Call +`initiateCall` resolves as soon as the call exists, before the caller's session has finished preparing: `call.session.prepareStatus` is `"pending"` at first. A failed prepare does not fail `initiateCall`, because the callee is already ringing; it shows on `call.session.prepareStatus` (`"failed"`) and `call.session.error`. After a failed prepare `call.session.join(container)` still performs a direct join, unless no call token came back with the call (it then resolves with `VALIDATION_ERROR`: generate a token and use `joinSession`, as `joinOutgoing` below does). -Start an outgoing call: + +**Always set the session type in `sessionSettings`, and `isPeerCall: true` for a 1:1 call.** The Calls SDK freezes the session settings when it prepares the session, and `session.join(container)` takes no settings: the ones you pass here are the ones the caller joins with. The default session type is video, so an audio call started without `sessionType: "VOICE"` opens the caller's camera. Without `isPeerCall`, a hang-up only removes one side from the room and the other side stays in it alone. + + +## Show "Ringing…" and detect the answer + +The caller stays on the session that `initiateCall` prepared and listens to it. Do not join yet: joining only when the callee has joined keeps the caller's camera and microphone off until there is someone to talk to. While the session is only prepared, the SDK still reports the callee's device reaching the room and the callee joining it. + +Listeners registered with `CometChatCalls.addEventListener` (or `call.session.addEventListener`) are global: they work before the prepare has finished and stay registered across sessions until you call the function they return. Register them right after `initiateCall` resolves, and check that the event belongs to the call on screen. ```javascript -async function initiateCall(receiverUid, callType = "video") { - const call = new CometChat.Call( - receiverUid, - callType === "video" ? CometChat.CALL_TYPE.VIDEO : CometChat.CALL_TYPE.AUDIO, - CometChat.RECEIVER_TYPE.USER - ); +// State of the outgoing call on screen. +let answered = false; +let cancelled = false; +let ended = false; +let stopWatching = () => {}; + +function watchOutgoingCall(call, container, isGroup) { + const me = CometChatCalls.getLoggedInUser()?.uid; + answered = cancelled = ended = false; + const done = () => answered || cancelled || ended; + + // Every way the ring can end goes through here, once. + const finish = (endText) => { + if (done()) return; + ended = true; + stopWatching(); + closeOutgoingScreen(endText); + }; + + // The callee's device received the call and prepared the session: it is ringing there. + const offConnected = CometChatCalls.addEventListener("onParticipantConnected", (participant) => { + if (!done() && participant.uid !== me) setStatus("Ringing…"); // not one of your own devices + }); + + // The callee answered: now the caller joins. + const offJoined = CometChatCalls.addEventListener("onParticipantJoined", () => { + if (done()) return; + answered = true; + stopWatching(); + joinOutgoing(call, container); + }); + + // The prepared session closed before anyone joined: the call is over. + const offClosed = CometChatCalls.addEventListener("onConnectionClosed", async () => { + if (done()) return; + // The close carries no reason: ask the server (see the next section). + finish(outgoingRingIsOver(await fetchCallStatus(call.sessionId)) ?? "Call ended"); + }); + + // Read the call's status every 3 s (see the next section). + const poll = setInterval(() => checkOutgoingStatus(call, container, finish), 3000); + + // Give up after 60 s (see Ring timeout). + const ringTimer = setTimeout(() => { + if (done()) return; + // A group call is withdrawn ("cancelled"): "unanswered" is a 1:1 outcome. + CometChatCalls.endCall(call.sessionId, isGroup ? "cancelled" : "unanswered"); + finish("No answer"); + }, 60_000); + + stopWatching = () => { + offConnected(); + offJoined(); + offClosed(); + clearInterval(poll); + clearTimeout(ringTimer); + }; +} - try { - const outgoingCall = await CometChat.initiateCall(call); - console.log("Call initiated:", outgoingCall); - - // Show outgoing call UI - showOutgoingCallScreen(outgoingCall); - - return outgoingCall; - } catch (error) { - console.error("Call initiation failed:", error); - throw error; +async function joinOutgoing(call, container) { + const { error } = await call.session.join(container); + if (error?.name === "VALIDATION_ERROR") { + // No call token came with the call: generate one and join directly. + const { token } = await CometChatCalls.generateToken(call.sessionId); + await CometChatCalls.joinSession(token, sessionSettings, container); + } else if (error) { + console.error("Join failed:", error); } } ``` -By default, an unanswered call automatically cancels after 45 seconds. You can customize this duration by passing an optional `timeout` parameter (in seconds) as the second argument to `initiateCall()`. +| Event | On the prepared session it means | +|-------|----------------------------------| +| `onConnected` | Your own prepare finished; the join will be near-instant | +| `onParticipantConnected(participant)` | The callee's device prepared the session: it is ringing there. `participant` carries `uid`, `name`, `pid` and `avatar` | +| `onParticipantJoined(participant)` | The callee answered. Join now | +| `onConnectionClosed` | The call ended before anyone joined (declined, cancelled, the room went away, or the prepared session expired after 5 minutes). It carries no reason. `onSessionTimedOut` is not part of this: it only reports an idle joined session | + +`call.session.join(container)` called while the prepare is still running waits for it. See [Join the session](#join-the-session). + +These events come from the room, so they only arrive while the caller's session is in it. If the caller's own prepare failed (`call.session.prepareStatus === "failed"`), neither arrives: the status poll below then reports the answer as `ongoing`. + +## Detect a decline, busy or no answer + +Because the caller is not reliably told when the callee declines, read the call's status from the server every 3 seconds from the moment `initiateCall` resolves until the call is answered, cancelled or times out (`watchOutgoingCall` above starts and stops the poll). Start at once, not only after "Ringing…": a callee who declines before their device prepared the session never sends `onParticipantConnected`. + +The status is the call's entry in [Call Logs](/calls/javascript/call-logs), read with the logged-in user's auth token: - - ```javascript -const receiverID = "UID"; -const callType = CometChat.CALL_TYPE.VIDEO; -const receiverType = CometChat.RECEIVER_TYPE.USER; - -const call = new CometChat.Call(receiverID, callType, receiverType); - -// Set a custom timeout of 60 seconds -CometChat.initiateCall(call, 60).then( - (outgoingCall) => { - console.log("Call initiated with 60s timeout:", outgoingCall); - }, - (error) => { - console.log("Call initiation failed:", error); +const APP_ID = "APP_ID"; +const REGION = "us"; // your app's region, in lower case + +async function fetchCallStatus(sessionId) { + const authToken = CometChatCalls.getUserAuthToken(); + if (!authToken) return null; + const url = `https://${APP_ID}.call-${REGION}.cometchat.io/v3.0/calls/${encodeURIComponent(sessionId)}`; + try { + const response = await fetch(url, { + headers: { accept: "application/json", appid: APP_ID, authtoken: authToken }, + }); + if (!response.ok) return null; + const body = await response.json(); + const entry = Array.isArray(body.data) ? body.data[0] : body.data; + return entry?.status ?? null; + } catch { + return null; // offline or a failed request: keep ringing } -); -``` - - -```typescript -const receiverID: string = "UID"; -const callType: string = CometChat.CALL_TYPE.VIDEO; -const receiverType: string = CometChat.RECEIVER_TYPE.USER; - -const call: CometChat.Call = new CometChat.Call(receiverID, callType, receiverType); - -// Set a custom timeout of 60 seconds -CometChat.initiateCall(call, 60).then( - (outgoingCall: CometChat.Call) => { - console.log("Call initiated with 60s timeout:", outgoingCall); - }, - (error: CometChat.CometChatException) => { - console.log("Call initiation failed:", error); +} + +// What the caller's screen says, or null while the call still rings. +function outgoingRingIsOver(status) { + if (!status) return null; + switch (status.toLowerCase()) { + case "initiated": + case "ongoing": // answered: handled separately + return null; + case "rejected": + return "Declined"; + case "busy": + return "Busy"; + default: + return "Call ended"; } -); +} + +// Called every 3 s by watchOutgoingCall until the ring ends. +async function checkOutgoingStatus(call, container, finish) { + const status = await fetchCallStatus(call.sessionId); + if (answered || cancelled || ended) return; + + if (status?.toLowerCase() === "ongoing" && call.session.prepareStatus === "failed") { + // Answered, but the caller is not in the room to hear onParticipantJoined. + answered = true; + stopWatching(); + joinOutgoing(call, container); + return; + } + const endText = outgoingRingIsOver(status); + if (endText) finish(endText); +} ``` - - -| Parameter | Type | Description | -| ---------- | -------- | --------------------------------------------------------------------------------------------------------------- | -| `call` | `Call` | The call object created with receiver ID, call type, and receiver type. | -| `timeout` | `number` | Optional. The ringing duration in seconds before an unanswered call is automatically cancelled. Default: `45`. | +Never end a call because the status could not be read: `null` means "keep ringing". The Chat SDK's `CallListener.onOutgoingCallRejected` can arrive as well, carrying the status (`rejected`, `busy`, or `unanswered`), but not in every case, so keep the poll (see [Receive a call](#receive-a-call) for filtering it). -## Listen for Incoming Calls +## Receive a call -Register a call listener to receive incoming calls: +On the web an incoming call arrives as a call message on the Chat SDK's real-time connection. Register a `CallListener` once, after the Chat SDK login, and remove it at logout. ```javascript -const callListenerId = "CALL_LISTENER_ID"; +import { CometChat } from "@cometchat/chat-sdk-javascript"; + +const LISTENER_ID = "ringing"; +const myUid = () => CometChatCalls.getLoggedInUser()?.uid; CometChat.addCallListener( - callListenerId, + LISTENER_ID, new CometChat.CallListener({ - onIncomingCallReceived: (incomingCall) => { - console.log("Incoming call:", incomingCall); - showIncomingCallScreen(incomingCall); + onIncomingCallReceived: (call) => { + // The caller's own tabs receive their 1:1 call as "incoming" too. + if (call.getCallInitiator()?.getUid() === myUid()) return; + + // A message delivered late (a sleeping tab, a reconnect) for a call that stopped ringing long ago. + const initiatedAt = call.getInitiatedAt(); // seconds + if (initiatedAt && Date.now() / 1000 - initiatedAt > 60) return; + + const isGroup = call.getReceiverType() === CometChat.RECEIVER_TYPE.GROUP; + if (isBusy()) { + // A 1:1 caller is told you are busy. A group call is just not shown. + if (!isGroup) CometChatCalls.endCall(call.getSessionId(), "busy"); + return; + } + ringIncomingCall(call); }, - onOutgoingCallAccepted: (acceptedCall) => { - console.log("Call accepted:", acceptedCall); - startCallSession(acceptedCall.getSessionId()); + // The caller cancelled, or gave up ("unanswered"). A cancel reaches the caller's own tabs too. + onIncomingCallCancelled: (call) => { + stopRinging(call.getSessionId(), "over"); }, - onOutgoingCallRejected: (rejectedCall) => { - console.log("Call rejected:", rejectedCall); - hideOutgoingCallScreen(); - }, - onIncomingCallCancelled: (cancelledCall) => { - console.log("Call cancelled:", cancelledCall); - hideIncomingCallScreen(); - }, - onCallEndedMessageReceived: (endedCall) => { - console.log("Call ended:", endedCall); + // A 1:1 "rejected" / "busy" reaches every tab of both users; "unanswered" reaches the caller. + onOutgoingCallRejected: (call) => { + if (call.getCallInitiator()?.getUid() === myUid()) { + // Your own outgoing call: call.getStatus() is "rejected", "busy" or "unanswered". + endOutgoingIfShowing(call.getSessionId(), call.getStatus()); + } else { + // Declined on another of your tabs or devices. + stopRinging(call.getSessionId(), "over"); + } }, }) ); + +// At logout: +CometChat.removeCallListener(LISTENER_ID); ``` -## Accept an Incoming Call +Your app does the ringing itself: play a ringtone, show the incoming-call screen, and, if the tab is in the background, show a browser notification (request the `Notification` permission on a user gesture, for example when the user places their first call). Browsers may block audio that starts without a user gesture on the page. + +`endCall(sessionId, "busy")` while the user is in another call leaves that other call untouched: `endCall` only tears down the live session when it is the call being ended. + +## Prepare the ringing call + +As soon as the call rings, prepare its session with `CometChatCalls.connect`. Preparing is signalling only: no camera or microphone is opened and the caller does not see the callee in the call, but it is what turns the caller's screen to "Ringing…" and makes the answer near-instant. In a 1:1 call (`isPeerCall: true`) it also lets the caller's cancel reach this tab as `onConnectionClosed`, because the caller leaving a prepared peer session ends the room. ```javascript -async function acceptCall(incomingCall) { - try { - const acceptedCall = await CometChat.acceptCall(incomingCall.getSessionId()); - console.log("Call accepted:", acceptedCall); - - // Start the call session - startCallSession(acceptedCall.getSessionId()); - } catch (error) { - console.error("Accept call failed:", error); - } +// The incoming call on screen. state: "ringing" | "answered" | "over" | "elsewhere" +let ring = null; + +function ringIncomingCall(call) { + const thisRing = { call, state: "ringing" }; + ring = thisRing; + const sessionId = call.getSessionId(); + showIncomingScreen(call); + startRingtone(); + startIncomingRingTimer(sessionId); + + CometChatCalls.connect(sessionId, { + sessionType: call.getType() === "audio" ? "VOICE" : "VIDEO", + isPeerCall: call.getReceiverType() !== CometChat.RECEIVER_TYPE.GROUP, + }) + .then((session) => { + // The ring ended without an answer while the prepare ran: release it. + if (thisRing.state === "over" && session.prepareStatus === "prepared" && !session.isSessionJoined()) { + session.leave(); + } + }) + .catch((error) => { + // Answer falls back to a direct join; the status poll keeps watching the ring. + console.warn("Preparing the call failed:", error); + }); } ``` -## Reject an Incoming Call +`connect(sessionId, sessionSettings)` resolves with the `CallSession` once the prepare has settled: `session.prepareStatus` is `"prepared"`, or `"cancelled"` when `leave()` was called meanwhile or a later `connect` for another session replaced it. Calling it again for the same session while it is preparing or prepared returns the same prepare. It rejects when the session could not be prepared, with one of two shapes: + +- `{ errorCode, errorDescription }` before the prepare starts: `ERROR_SESSION_ID_MISSING`, `ERROR_USER_NOT_LOGGED_IN`, or a failed call token request. +- `{ name, message }` (`PrepareSessionException`) for everything after: a token verification, connection or conference failure, or `SESSION_ALREADY_ACTIVE` while another session is joined. The same error is left on `session.error`. + +The settings you pass to `connect` are frozen: they are the ones the callee joins with. A prepared session that nobody joins within 5 minutes is torn down: `onConnectionClosed` fires and `getActiveSession()` returns `null`, while `session.prepareStatus` stays `"prepared"` (it is `"failed"` with `PREPARE_TIMEOUT` only if the prepare had not finished connecting by then). `onSessionTimedOut` does not fire for it. + + +**Leaving a prepared 1:1 session ends the room for everyone in it.** With `isPeerCall: true`, tearing down a prepared session that is in the room, whether by `leave()`, `endCall`, a `connect` or `initiateCall` for another session, or the 5-minute expiry, asks the server to end the room, not only to remove this tab. That is how a cancel or decline reaches the other side, and it does no harm once the ring is over. + +It is a problem when the call was answered in another tab or on another device: that tab's prepared session is still in the room of the live call. Do not call `leave()` on it. That only delays the problem: the 5-minute expiry, or the next `connect` or `initiateCall` in that tab, tears the session down the same way. The Calls SDK has no local-only leave for a prepared 1:1 session, so there is no client-side way around this. + + +### Stop ringing on the callee + +Stop ringing when any of these happens for the ringing call: + +- `onIncomingCallCancelled` arrives on the Chat SDK listener (the caller cancelled or gave up). +- `onOutgoingCallRejected` arrives for a call you did not place (you declined it in another tab or on another device). +- `onConnectionClosed` fires while the call is still ringing and this tab had prepared it (in a 1:1 call: the caller cancelled, or the call ended). +- Your own ring timer runs out. +- Another of your tabs tells you it answered or declined the call (see [Several tabs and devices](#several-tabs-and-devices)). +- The call's status is no longer `initiated`. Poll it every 3 seconds while ringing, with the `fetchCallStatus` function above, as a backstop: a cancel does not always reach a ringing tab as a message, and `onConnectionClosed` only fires when the room is destroyed. ```javascript -async function rejectCall(incomingCall, reason = CometChat.CALL_STATUS.REJECTED) { - try { - await CometChat.rejectCall(incomingCall.getSessionId(), reason); - console.log("Call rejected"); - hideIncomingCallScreen(); - } catch (error) { - console.error("Reject call failed:", error); +// "over": the ring ended without an answer. "elsewhere": answered on another tab or device. +function stopRinging(sessionId, reason) { + if (!ring || ring.state !== "ringing" || ring.call.getSessionId() !== sessionId) return; + ring.state = reason; + stopRingtone(); + stopIncomingRingTimer(); + closeIncomingScreen(); + // Release the prepared session only when the call is over (see the warning above). + const session = CometChatCalls.getActiveSession(); + if (reason === "over" && session?.sessionId === sessionId && !session.isSessionJoined()) { + session.leave(); } } + +// From the status poll. A 1:1 call stops on anything but "initiated"; "ongoing" means it was answered +// on another of the user's devices. A group call keeps ringing while it goes on, until it is over. +function ringStatusReason(status, isGroup) { + if (!status) return null; // could not read it: keep ringing + const s = status.toLowerCase(); + if (isGroup) return ["ended", "cancelled", "unanswered", "rejected"].includes(s) ? "over" : null; + if (s === "initiated") return null; + return s === "ongoing" ? "elsewhere" : "over"; +} ``` -## Cancel an Outgoing Call +## Answer a call + +There is no separate "accept" call. Answering is joining the session the tab prepared; if the prepare failed, join directly. ```javascript -async function cancelCall(outgoingCall) { - try { - await CometChat.rejectCall(outgoingCall.getSessionId(), CometChat.CALL_STATUS.CANCELLED); - console.log("Call cancelled"); - hideOutgoingCallScreen(); - } catch (error) { - console.error("Cancel call failed:", error); +async function answer(container) { + if (!ring || ring.state !== "ringing") return; + ring.state = "answered"; + const call = ring.call; + const sessionId = call.getSessionId(); + stopRingtone(); + stopIncomingRingTimer(); + tellOtherTabs("handled", sessionId); + + // The session this tab prepared: join waits for a prepare still running, and joins + // directly with the frozen settings if the prepare failed after it started. + const session = CometChatCalls.getActiveSession(); + if (session?.sessionId === sessionId) { + const { error } = await session.join(container); + if (!error) return; + if (error.name !== "VALIDATION_ERROR") return console.error("Join failed:", error); + // VALIDATION_ERROR: no call token could be generated for the prepare; join directly below. } + + // Nothing prepared for this call (the prepare failed or never ran): join directly. + const { token } = await CometChatCalls.generateToken(sessionId); + const { error } = await CometChatCalls.joinSession(token, { + sessionType: call.getType() === "audio" ? "VOICE" : "VIDEO", + isPeerCall: call.getReceiverType() !== CometChat.RECEIVER_TYPE.GROUP, + }, container); + if (error) console.error("Join failed:", error); } ``` -## Start the Call Session +The browser asks for the camera and microphone when the session is joined, not when it is prepared. -Once the call is accepted, join the session using the Calls SDK: +## Join the session + +Both sides join the same way: the callee when the user answers, the caller from `onParticipantJoined`. `session.join(container)` mounts the call UI into `container` and takes no settings. + +| State of the session | What `session.join(container)` does | +|----------------------|--------------------------------------| +| Prepared, or still preparing | The actual join, near-instant. `onSessionJoined` fires, then the camera and microphone open | +| The prepare failed or expired, and nothing is live | A normal direct join with the settings frozen at prepare. If no call token was generated (`TOKEN_GENERATION_FAILED`, or none came back with `initiateCall`), it resolves with `VALIDATION_ERROR`: use `generateToken` and `joinSession` instead | +| The prepare was cancelled (`leave()`) | Resolves with `SESSION_CANCELLED`: call `connect` again, or join directly | +| Already joined | Resolves with success and does nothing | + +`session.join` resolves with `{ data, error }` like `joinSession`. `CometChatCalls.joinSession(token, settings, container)` refuses to run while a session is preparing or prepared (`SESSION_ALREADY_PREPARED`): join the prepared one with `session.join(container)`, or cancel it with `session.leave()` first. + + +`CometChatCalls.getActiveSession()` returns the SDK's one `CallSession` while a session is preparing, prepared, being joined or joined: from the moment `connect` is called, or from the moment `initiateCall` has created the call. It returns `null` otherwise, including after a failed or cancelled prepare and after the 5-minute expiry. Always check it for `null` (`getActiveSession()?.leave()`), and compare `session.sessionId` with the call you mean: a later `connect` for another session re-points the same object, so an old reference describes the newer session. `sessionId` is set only for a session from `connect` or `initiateCall`; after a direct `joinSession` it is `null`. + + +See [Join Session](/calls/javascript/join-session) for the container, [Session Settings](/calls/javascript/session-settings) for layouts and controls, and [Events](/calls/javascript/events) for everything the joined session reports. + +## Decline a call + +Decline a 1:1 call with `endCall` and the `rejected` status. When this tab had prepared the session, `endCall` first starts tearing it down (in a 1:1 call that asks the server to end the room), then tells the server the status. `onConnectionClosed` follows when the room closes. ```javascript -async function startCallSession(sessionId) { - const container = document.getElementById("call-container"); - - try { - // Generate token - const tokenResult = await CometChatCalls.generateToken(sessionId); - - // Join session - await CometChatCalls.joinSession( - tokenResult.token, - { - sessionType: "VIDEO", - layout: "TILE", - }, - container - ); - - // Listen for session end - CometChatCalls.addEventListener("onSessionLeft", () => { - endCall(sessionId); - }); - } catch (error) { - console.error("Failed to start call session:", error); +async function decline() { + if (!ring || ring.state !== "ringing") return; + ring.state = "over"; + const call = ring.call; + stopRingtone(); + stopIncomingRingTimer(); + closeIncomingScreen(); + tellOtherTabs("handled", call.getSessionId()); + + if (call.getReceiverType() === CometChat.RECEIVER_TYPE.GROUP) { + // A group call's decline is local: the call carries on for the other members. + const session = CometChatCalls.getActiveSession(); + if (session?.sessionId === call.getSessionId()) session.leave(); + return; } + const { error } = await CometChatCalls.endCall(call.getSessionId(), "rejected"); + if (error) console.warn("Decline failed:", error); } ``` -## End the Call +Use `"busy"` instead of `"rejected"` when the user is already in another call. + +`endCall(sessionId, status)` never rejects. It resolves with `{ data: undefined, error: null }` once the server has the new status, or with `error` set: `USER_NOT_LOGGED_IN`, `VALIDATION_ERROR` (no session ID), or the failure of the request. The status defaults to `"rejected"`. + +## Cancel a call + +The caller ends a call that is still ringing with `endCall` and the `cancelled` status. The SDK first starts tearing the caller's prepared session down, then tells the server; `onConnectionClosed` follows when the room closes (the sample below has already removed its own listener). The callee's ring stops. ```javascript -async function endCall(sessionId) { - try { - // Leave the Calls SDK session - CometChatCalls.leaveSession(); - - // End the call in Chat SDK - await CometChat.endCall(sessionId); - - console.log("Call ended"); - } catch (error) { - console.error("End call failed:", error); - } +async function cancelOutgoingCall(sessionId) { + cancelled = true; + stopWatching(); + closeOutgoingScreen(); // close locally whatever the server says + const { error } = await CometChatCalls.endCall(sessionId, "cancelled"); + if (error) console.warn("Cancel failed:", error); } ``` -## Incoming Call UI Example +Disable the cancel button after the first click, and ignore an `onParticipantJoined` that arrives after you cancelled: the cancel wins. If the user cancels before `initiateCall` has resolved, cancel the call when it resolves, or the callee rings for a call nobody is waiting on: ```javascript -function showIncomingCallScreen(call) { - const caller = call.getCallInitiator(); - - const html = ` -
-
- ${caller.getName()} -

${caller.getName()}

-

Incoming ${call.getType()} call...

-
- - -
-
-
- `; - - document.body.insertAdjacentHTML("beforeend", html); - window.currentCall = call; - - // Play ringtone - playRingtone(); -} - -function hideIncomingCallScreen() { - document.getElementById("incoming-call")?.remove(); - stopRingtone(); +let callSeq = 0; + +async function startCall(receiverUid, options, container) { + const seq = ++callSeq; + const { data: call } = await CometChatCalls.initiateCall(receiverUid, options); + if (seq !== callSeq) { + // Cancelled while "Calling…" (the cancel button did ++callSeq): the call exists now, so withdraw it. + if (call) CometChatCalls.endCall(call.sessionId, "cancelled"); + return; + } + if (call) watchOutgoingCall(call, container, options.receiverType === "group"); } ``` -## Outgoing Call UI Example +## Ring timeout + +Nothing in the web SDKs ends a ring on a timer, so run a timer on both sides. + +On the caller, start it when `initiateCall` resolves and end the call as `unanswered` when it runs out; `watchOutgoingCall` above does this. The Connect web app uses 60 seconds. A callee on a phone stops ringing at its Push SDK's ring window, but if the callee's device is never reached, nothing else ends the caller's ringing until the prepared session expires after 5 minutes. A group call the caller stops waiting on is withdrawn as `cancelled` instead: `unanswered` is a 1:1 outcome. + +On the callee, stop ringing a little after the caller would have given up (the Connect web app uses 65 seconds) with `stopRinging(sessionId, "over")`. Do not send a status: the caller ends the call. + +## End a call + +Hang up by leaving the session with `session.leave()` or `CometChatCalls.leaveSession()`. The other side receives `onParticipantLeft`, and in a peer call (`isPeerCall: true`) the session ends for both. No status needs to be sent and the call is logged by the backend. ```javascript -function showOutgoingCallScreen(call) { - const receiver = call.getCallReceiver(); - - const html = ` -
-
- ${receiver.getName()} -

${receiver.getName()}

-

Calling...

- -
-
- `; - - document.body.insertAdjacentHTML("beforeend", html); - window.currentCall = call; - - // Play ringback tone - playRingbackTone(); +// The default call UI's Leave button leaves the session itself; only clean up here. +CometChatCalls.addEventListener("onLeaveSessionButtonClicked", () => { + closeCallScreen(); +}); + +// Your own hang-up control: +async function hangUp() { + await CometChatCalls.getActiveSession()?.leave(); + closeCallScreen(); } -function hideOutgoingCallScreen() { - document.getElementById("outgoing-call")?.remove(); - stopRingbackTone(); -} +// The other participant ended the call: +CometChatCalls.addEventListener("onSessionLeft", closeCallScreen); +CometChatCalls.addEventListener("onConnectionClosed", closeCallScreen); ``` -## Cleanup +`onConnectionClosed` fires for a joined call that ends and for a prepared session that is torn down, so check which screen is showing before acting on it. -Remove the call listener when no longer needed: +## Several tabs and devices -```javascript -CometChat.removeCallListener(callListenerId); -``` +Every open tab where the callee is logged in receives the call and rings. Answering or declining in one tab does not stop the others by itself: + +- Tell your other tabs, for example with a [`BroadcastChannel`](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel) (`tellOtherTabs` in the samples above). A tab that hears another tab answered calls `stopRinging(sessionId, "elsewhere")`, which does not call `leave()` on its prepared session, because releasing a prepared 1:1 session ends the room. The session still expires after 5 minutes, with the same effect (see the warning under [Prepare the ringing call](#prepare-the-ringing-call)). A tab that hears another tab declined calls `stopRinging(sessionId, "over")`. +- A 1:1 decline also reaches your other tabs as `onOutgoingCallRejected`, as shown in [Receive a call](#receive-a-call). +- The status poll is the backstop: a call answered on another of the user's devices reads `ongoing`, which stops a 1:1 ring as `"elsewhere"`. +- While one tab is in a call, the Connect web app keeps the other tabs from ringing; the tab in the call declines a 1:1 call with `busy`. + +A tab that is already joined to a call cannot prepare another one: `connect` rejects with `SESSION_ALREADY_ACTIVE` and `initiateCall` returns `SESSION_ALREADY_ACTIVE`. + +## Group calls + +To ring a group, call `initiateCall` with the group's GUID as the receiver and `receiverType: "group"`, and leave `isPeerCall` off so the call outlives any one member leaving. Every member's open tabs (and phones) ring. When a group call rings, declining it or letting it ring out sends nothing: the call carries on for the other members, and the decline is local to that tab. You can also start a group call without ringing: share a session ID (for example in a custom message) and have each member join it with [`joinSession`](/calls/javascript/join-session). + +## Logout + +End or cancel any call of your own before logging out: `endCall` needs a logged-in user. Then remove the `CallListener`, stop any ring, and log out of both SDKs (`CometChatCalls.logout()` and `CometChat.logout()`). + +## Call status values -## Related Documentation +| Status | Meaning | Sent by | +|--------|---------|---------| +| `initiated` | Created, ringing | Backend, on `initiateCall` | +| `ongoing` | Answered | Backend, when the callee joins | +| `rejected` | Callee declined | Callee, `endCall(sessionId, "rejected")` | +| `busy` | Callee was in another call | Callee, `endCall(sessionId, "busy")` | +| `cancelled` | Caller ended it before an answer | Caller, `endCall(sessionId, "cancelled")` | +| `unanswered` | Nobody answered in the ring window | Caller's timer, `endCall(sessionId, "unanswered")`, or a mobile callee's Push SDK | +| `ended` | Hung up after being answered | Backend, when the session ends | +| `missed` | How an unanswered or cancelled incoming call appears in call logs | Call logs | -- [CometChat Chat SDK - Calling](/sdk/javascript/calling-overview) -- [Join Session](/calls/javascript/join-session) +## Related +- [Migrating from legacy ringing](/calls/javascript/migrating-from-legacy-ringing). +- [Events](/calls/javascript/events), [Actions](/calls/javascript/actions), [Call Logs](/calls/javascript/call-logs). +- Ringing on mobile: [Android](/calls/android/ringing) and [iOS](/calls/ios/ringing). diff --git a/calls/javascript/session-settings.mdx b/calls/javascript/session-settings.mdx index 2dd872f3b..b383f7d5a 100644 --- a/calls/javascript/session-settings.mdx +++ b/calls/javascript/session-settings.mdx @@ -8,7 +8,7 @@ description: "Configure CometChat Calls SDK v5 session settings on JavaScript fo The session settings object allows you to customize every aspect of your call session before participants join. From controlling the initial audio/video state to customizing the UI layout and hiding specific controls, these settings give you complete control over the call experience. -These are pre-session configurations that must be set before joining a call. Once configured, pass the settings object to the `joinSession()` method. Settings cannot be changed after the session has started, though many features can be controlled dynamically during the call using call actions. +These are pre-session configurations that must be set before joining a call. Once configured, pass the settings object to the `joinSession()` method, or, for a [ringing](/calls/javascript/ringing) call, to `initiateCall()` (as `sessionSettings`) or `connect()`. A session prepared by `initiateCall()` or `connect()` keeps the settings it was prepared with: `session.join(container)` takes none. Settings cannot be changed after the session has started, though many features can be controlled dynamically during the call using call actions. ```javascript @@ -43,6 +43,18 @@ sessionType: "VIDEO" **Default:** `VIDEO` +### Peer Call + +**Property:** `isPeerCall` + +Marks the session as a one-to-one call. In a peer call, when one participant leaves, the session ends for both instead of continuing with one person in it. The SDK also watches the other participant's connection and reports `onRemoteConnectionLost` when no media statistics arrive from them for 10 seconds, and `onRemoteConnectionRestored` when they resume. Set it for every 1:1 ringing call; leave it off for group calls and meetings. + +```javascript +isPeerCall: true +``` + +**Default:** `false` + ### Layout Mode **Property:** `layout` @@ -339,6 +351,7 @@ const callSettings = { | Property | Type | Default | Description | |----------|------|---------|-------------| | `sessionType` | String | `VIDEO` | Call type: `VIDEO` or `VOICE` | +| `isPeerCall` | Boolean | `false` | One-to-one call: the session ends for both when either participant leaves | | `layout` | String | `TILE` | Layout: `TILE`, `SIDEBAR`, or `SPOTLIGHT` | | `startAudioMuted` | Boolean | `false` | Start with microphone muted | | `startVideoPaused` | Boolean | `false` | Start with camera off | diff --git a/calls/javascript/setup.mdx b/calls/javascript/setup.mdx index 144cb5617..18eadee57 100644 --- a/calls/javascript/setup.mdx +++ b/calls/javascript/setup.mdx @@ -2,7 +2,7 @@ title: "Setup" sidebarTitle: "Setup" sdk_version: "5.x" -description: "Set up CometChat Calls SDK v5 for JavaScript with installation, app credentials, initialization, authentication, and a basic calling flow." +description: "Set up CometChat Calls SDK v5 for JavaScript with installation, app credentials, initialization, authentication, and the Chat SDK needed for ringing." --- @@ -27,16 +27,22 @@ Install the CometChat Calls SDK using npm or yarn: ```bash -npm install @cometchat/calls-sdk-javascript@5.0.5 +npm install @cometchat/calls-sdk-javascript@5.1.0 ``` ```bash -yarn add @cometchat/calls-sdk-javascript@5.0.5 +yarn add @cometchat/calls-sdk-javascript@5.1.0 ``` + + **Ringing users?** On the web, an incoming call reaches an open tab through the Chat SDK's call listener, so a + ringing app also needs `@cometchat/chat-sdk-javascript`, initialized and logged in as the same user. The Calls SDK + starts, prepares, joins and ends the call. See [Ringing](/calls/javascript/ringing). + + ## Import the SDK Import the `CometChatCalls` class in your JavaScript or TypeScript file: diff --git a/calls/javascript/troubleshooting.mdx b/calls/javascript/troubleshooting.mdx index da1e6de6b..37ee14f54 100644 --- a/calls/javascript/troubleshooting.mdx +++ b/calls/javascript/troubleshooting.mdx @@ -77,6 +77,23 @@ sdk_version: "5.x" --- +## Ringing + +| Symptom | Cause | Fix | +| --- | --- | --- | +| Incoming calls do not ring in the browser | The Chat SDK is not logged in, is logged in as another user, or no `CometChat.addCallListener` is registered | Initialize and log in the Chat SDK as the same user as the Calls SDK, and register the `CallListener` after login. A closed tab never rings: the web has no VoIP push. See [Ringing](/calls/javascript/ringing) | +| Every outgoing call is declined "busy" at once | The caller's own tab treats its outgoing call as incoming (the Chat SDK delivers a 1:1 call message to the caller too) and declines it as busy | In `onIncomingCallReceived`, skip calls whose `getCallInitiator().getUid()` is the logged-in user | +| Caller never sees "Ringing…" or "answered" | The callee never prepared the session, the caller's own prepare failed (`call.session.prepareStatus === "failed"`, so it is not in the room), or the listeners were registered too late | Have the callee call `connect(sessionId, settings)` as soon as the call rings; register the caller's listeners right after `initiateCall` resolves; poll the call's status, which reports an answer as `ongoing` | +| Caller keeps ringing after the callee declined | A busy decline, or a decline before the callee's device connected, does not close the caller's prepared session | Read the call's status every 3 s while ringing and run a ring timer that ends the call as `unanswered` | +| Callee keeps ringing after the caller cancelled | The cancel did not reach the tab as `onIncomingCallCancelled`, and the room was not destroyed | Poll the call's status while ringing, and stop on anything but `initiated` for a 1:1 call | +| Audio call opens the camera | Session type not set when the session was prepared | Pass `sessionType: "VOICE"` in `initiateCall`'s `sessionSettings` and to `connect`; `session.join(container)` takes no settings | +| One side stays in the call after the other hangs up | The session was not prepared or joined as a peer call | Pass `isPeerCall: true` for 1:1 calls | +| A call answered in another tab or on another device ends | A tab released its prepared 1:1 session after the call was answered elsewhere: leaving a prepared peer session ends the room | Stop ringing without `leave()` when the call was answered elsewhere. The prepared session still expires after 5 minutes, or is torn down by the next `connect` or `initiateCall` in that tab, with the same effect; there is no local-only leave. See [Ringing](/calls/javascript/ringing#prepare-the-ringing-call) | +| `joinSession()` returns `SESSION_ALREADY_PREPARED` | A session is prepared for a ringing call | Join it with `session.join(container)`, or cancel it with `session.leave()` first | +| `onSessionTimedOut` never fires for an unanswered call | A prepared session that nobody joins reports `onConnectionClosed` after 5 minutes | Listen for `onConnectionClosed`, and use your own ring timer | + +--- + ## Events & Callbacks | Symptom | Cause | Fix | diff --git a/docs.json b/docs.json index 96952f214..05af6ebb7 100644 --- a/docs.json +++ b/docs.json @@ -5368,7 +5368,8 @@ { "group": "Migration Guide", "pages": [ - "calls/javascript/migration-guide-v5" + "calls/javascript/migration-guide-v5", + "calls/javascript/migrating-from-legacy-ringing" ] }, { diff --git a/sdk/javascript/all-real-time-listeners.mdx b/sdk/javascript/all-real-time-listeners.mdx index d6749df14..d4facc10c 100644 --- a/sdk/javascript/all-real-time-listeners.mdx +++ b/sdk/javascript/all-real-time-listeners.mdx @@ -552,6 +552,10 @@ See [Pin A Conversation](/sdk/javascript/pin-conversation) for the full feature. ## Call Listener + +**Ringing now runs on the Calls SDK.** The Chat SDK's calling methods (`CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`) are superseded by `CometChatCalls.initiateCall`, `connect`, `session.join(container)` and `endCall(sessionId, status)` in the Calls SDK 5.1. The `CallListener` stays in use on the web: it is how an incoming call reaches an open tab (`onIncomingCallReceived`, `onIncomingCallCancelled`). The caller's "answered" event is the Calls SDK's `onParticipantJoined` on the prepared session, and a decline is read from the call's status while it rings. See [Ringing](/calls/javascript/ringing) and [Migrating from Legacy Ringing](/calls/javascript/migrating-from-legacy-ringing). + + Receive events for incoming and outgoing call state changes. | Method | Information | diff --git a/sdk/javascript/calling-overview.mdx b/sdk/javascript/calling-overview.mdx index 04643ef3a..e41dbc066 100644 --- a/sdk/javascript/calling-overview.mdx +++ b/sdk/javascript/calling-overview.mdx @@ -6,13 +6,19 @@ description: "Add CometChat voice and video calling to JavaScript apps with one- CometChat's Calling SDK enables you to add voice and video calling to your JavaScript application. The Calling SDK works alongside the Chat SDK to provide a complete communication experience including 1-on-1 calls, group calls, and conference sessions. + +**The Chat SDK's calling methods are superseded.** `CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `startCall`, `getActiveCall`, `clearActiveCall` and the Chat SDK's `CallSettings` still work, but ringing now runs on the Calls SDK 5.1: `CometChatCalls.initiateCall`, `connect`, `session.join(container)`, `endCall(sessionId, status)` and `session.leave()`. See [Ringing](/calls/javascript/ringing) and [Migrating from Legacy Ringing](/calls/javascript/migrating-from-legacy-ringing). + +Not superseded: `CometChat.addCallListener` and `CallListener`, which still deliver an incoming call to an open tab on the web, and the `Call` message model and the `CometChat.CALL_STATUS` and `CometChat.CALL_TYPE` values, because call messages in a conversation's history still use them. + + ## Key Features - **Voice & Video Calls** — High-quality 1-on-1 and group calling with adaptive bitrate - **Screen Sharing** — Share your screen during calls for collaboration - **Call Recording** — Record calls and access recordings later - **Call Logs** — Track call history with detailed metadata -- **Ringing & Notifications** — Built-in call ringing flow with accept/reject +- **Ringing & Notifications** — Ring users in their open tabs and on their phones, with answer, decline, cancel and busy - **Customizable UI** — Presenter mode and video view customization ## Choose Your Version From 7171e06fee8a7aca7cac90b72ef65fb30e49fd26 Mon Sep 17 00:00:00 2001 From: Jitvar Patil Date: Wed, 21 Oct 2026 04:29:36 +0530 Subject: [PATCH 2/2] docs(calls/javascript): give up ringing after 45 s on the caller A callee on a phone rings for 60 s, so the caller gives up first at 45 s and the callee never rings for a call nobody is waiting on. Matches the iOS, Android and web ringing samples. The web callee stops ringing after 60 s. Co-Authored-By: Claude Opus 5.5 --- calls/javascript/migrating-from-legacy-ringing.mdx | 6 +++--- calls/javascript/ringing.mdx | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/calls/javascript/migrating-from-legacy-ringing.mdx b/calls/javascript/migrating-from-legacy-ringing.mdx index bf1586943..de6d3d028 100644 --- a/calls/javascript/migrating-from-legacy-ringing.mdx +++ b/calls/javascript/migrating-from-legacy-ringing.mdx @@ -80,7 +80,7 @@ Keep both SDKs initialized and logged in as the same user: the Calls SDK for `in | Legacy | Now | Notes | |--------|-----|-------| | `CometChat.initiateCall(new CometChat.Call(receiverId, callType, receiverType))` | `CometChatCalls.initiateCall(receiver, { type, receiverType, sessionSettings })` → `{ data, error }` | `data.session` is the caller's session, already preparing. Set `sessionType` in `sessionSettings`, and `isPeerCall: true` for a 1:1 call | -| `CometChat.initiateCall(call, timeout)` | No timeout parameter | Run your own timer on the caller and end the call with `endCall(sessionId, "unanswered")` (the Connect web app uses 60 s) | +| `CometChat.initiateCall(call, timeout)` | No timeout parameter | Run your own timer on the caller and end the call with `endCall(sessionId, "unanswered")` after 45 s, before a phone's 60-second ring runs out | | `onOutgoingCallAccepted(call)` | `CometChatCalls.addEventListener("onParticipantJoined", ...)` on the prepared session, then `data.session.join(container)` | New in-between event: `onParticipantConnected` = the callee's device is ringing | | `onOutgoingCallRejected(call)` | Read the call's status every 3 s while ringing; `onConnectionClosed` when the prepared session closes | Not reliably reported: a busy decline, or a decline before the callee's device connected, closes nothing. `onOutgoingCallRejected` may still arrive; keep it as an extra signal, and check `getCallInitiator()`: a 1:1 decline reaches the callee's other tabs too | | `CometChat.rejectCall(sessionId, CometChat.CALL_STATUS.CANCELLED)` | `CometChatCalls.endCall(sessionId, "cancelled")` | Tears the caller's prepared session down first | @@ -120,7 +120,7 @@ const offJoined = CometChatCalls.addEventListener("onParticipantJoined", () => { }); const offClosed = CometChatCalls.addEventListener("onConnectionClosed", () => closeOutgoingScreen()); const stopWatching = () => { offConnected(); offJoined(); offClosed(); }; -// Plus: poll the call's status every 3 s and a 60 s ring timer (see Ringing). +// Plus: poll the call's status every 3 s and a 45 s ring timer (see Ringing). function cancel() { stopWatching(); @@ -138,7 +138,7 @@ function cancel() { | `CometChat.acceptCall(sessionId)` then `generateToken` + `joinSession` | `session.join(container)` on the session `connect` prepared. Nothing else to send | If the prepare failed, `generateToken` + `joinSession` as before | | `CometChat.rejectCall(sessionId, CometChat.CALL_STATUS.REJECTED)` | `CometChatCalls.endCall(sessionId, "rejected")` | `"busy"` when the user is in another call. A group call's decline is local: `session.leave()` | | `onIncomingCallCancelled(call)` | Still fires (also when the caller gives up as `unanswered`); also `onConnectionClosed` on a prepared 1:1 session, and the call's status read while ringing | The status is the backstop: a cancel does not always reach a ringing tab as a message | -| — | Your own ring timer on the callee | The Connect web app stops ringing after 65 s | +| — | Your own ring timer on the callee | Stop ringing after 60 s, a little after the caller gives up at 45 s | diff --git a/calls/javascript/ringing.mdx b/calls/javascript/ringing.mdx index 716d3653e..a4f8e66e1 100644 --- a/calls/javascript/ringing.mdx +++ b/calls/javascript/ringing.mdx @@ -161,13 +161,13 @@ function watchOutgoingCall(call, container, isGroup) { // Read the call's status every 3 s (see the next section). const poll = setInterval(() => checkOutgoingStatus(call, container, finish), 3000); - // Give up after 60 s (see Ring timeout). + // Give up after 45 s (see Ring timeout). const ringTimer = setTimeout(() => { if (done()) return; // A group call is withdrawn ("cancelled"): "unanswered" is a 1:1 outcome. CometChatCalls.endCall(call.sessionId, isGroup ? "cancelled" : "unanswered"); finish("No answer"); - }, 60_000); + }, 45_000); stopWatching = () => { offConnected(); @@ -519,9 +519,9 @@ async function startCall(receiverUid, options, container) { Nothing in the web SDKs ends a ring on a timer, so run a timer on both sides. -On the caller, start it when `initiateCall` resolves and end the call as `unanswered` when it runs out; `watchOutgoingCall` above does this. The Connect web app uses 60 seconds. A callee on a phone stops ringing at its Push SDK's ring window, but if the callee's device is never reached, nothing else ends the caller's ringing until the prepared session expires after 5 minutes. A group call the caller stops waiting on is withdrawn as `cancelled` instead: `unanswered` is a 1:1 outcome. +On the caller, start it when `initiateCall` resolves and end the call as `unanswered` when it runs out; `watchOutgoingCall` above does this. Use 45 seconds: a callee on a phone rings for 60 seconds, and giving up first means the callee never rings for a call the caller has stopped waiting on. A callee on a phone stops ringing at its Push SDK's ring window, but if the callee's device is never reached, nothing else ends the caller's ringing until the prepared session expires after 5 minutes. A group call the caller stops waiting on is withdrawn as `cancelled` instead: `unanswered` is a 1:1 outcome. -On the callee, stop ringing a little after the caller would have given up (the Connect web app uses 65 seconds) with `stopRinging(sessionId, "over")`. Do not send a status: the caller ends the call. +On the callee, stop ringing after 60 seconds, a little after the caller would have given up, with `stopRinging(sessionId, "over")`. Do not send a status: the caller ends the call. ## End a call