Skip to content

Enhance call handling with state management and WebRTC integration#26

Open
harshlocham wants to merge 122 commits into
mainfrom
feat/WebRTC
Open

Enhance call handling with state management and WebRTC integration#26
harshlocham wants to merge 122 commits into
mainfrom
feat/WebRTC

Conversation

@harshlocham
Copy link
Copy Markdown
Owner

This pull request introduces the foundational backend and frontend logic for real-time call signaling and WebRTC integration, enabling audio/video call features in the chat application. The changes include new socket event handlers for call signaling on the server, a client-side Zustand call state store, custom React hooks for managing call state and signaling, and a WebRTC peer connection manager.

The most important changes are:

Backend: Call Signaling and State Management

  • Added comprehensive socket event handlers in call.handler.ts to support call initiation, offer/answer exchange, ICE candidate relay, call acceptance/rejection, ending, and reconnection, using user-specific rooms for targeted signaling. [1] [2]
  • Introduced new Redis key patterns and TTL constants in keys.ts for tracking call states and participants, laying groundwork for call lifecycle management and concurrency control.

Backend Integration

  • Registered the new callHandler in the socket server initialization to activate call signaling features. [1] [2]

Frontend: Call State Management

  • Implemented a Zustand-based call-store.ts to manage the local call state, including participants, call status, direction, media states, and error handling, with convenient selectors for UI logic.

Frontend: Call Signaling and WebRTC

  • Added useCallSignaling React hook for handling socket events and emitting signaling messages, integrating with the call store for state updates and error handling.
  • Added useCallState hook for easy access to call state and selectors from components.
  • Added PeerManager class and useWebRTC hook to encapsulate WebRTC peer connection logic, including ICE candidate handling, offer/answer flow, media stream management, and connection state changes. [1] [2]

Copilot AI review requested due to automatic review settings March 31, 2026 12:39
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Mar 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat-app Ready Ready Preview, Comment Apr 12, 2026 7:57am

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces foundational call signaling + WebRTC scaffolding to support audio/video calling in the chat app, including shared socket payload/types, a backend call signaling handler, and frontend state/hooks for signaling + peer connection management.

Changes:

  • Added shared socket event names and payload types for call lifecycle + signaling (offer/answer/ICE/accept/reject/end/state).
  • Registered a new socket callHandler and added Redis key/TTL primitives for call lifecycle tracking.
  • Implemented frontend call state (Zustand) plus hooks for call signaling and WebRTC peer connection management.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
packages/types/socket/payloads.ts Adds call-related payload/type definitions shared across client/server.
packages/types/socket/events.ts Adds call socket event constants + typed event maps.
packages/services/call.service.ts Adds call state machine helpers (transitions/terminal states).
apps/web/store/call-store.ts Introduces Zustand call store + selectors for UI state.
apps/web/lib/webrtc/peer-manager.ts Adds a wrapper around RTCPeerConnection lifecycle and signaling helpers.
apps/web/hooks/useWebRTC.ts Adds React hook integrating PeerManager + local/remote stream handling.
apps/web/hooks/useCallState.ts Adds convenience hook to access call store state/selectors.
apps/web/hooks/useCallSignaling.ts Adds hook to listen/emit call signaling socket events and update store.
apps/socket/server/socket/keys.ts Adds Redis key patterns + TTL constants for call state primitives.
apps/socket/server/socket/index.ts Registers callHandler on socket connection.
apps/socket/server/socket/handlers/call/call.handler.ts Implements server-side call signaling relays using user-specific rooms.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 61 to 70
socket.on(SocketEvents.CALL_OFFER, ({ to, offer }: CallOfferPayload) => {
io.to(to).emit(SocketEvents.CALL_OFFER, {
from: socket.data.userId,
const from = socket.data.userId;
if (!to) return;

io.to(userRoom(to)).emit(SocketEvents.CALL_OFFER, {
from,
to,
offer,
});
});
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CALL_OFFER handler forwards only {from,to,offer} and drops call-scoping metadata already defined in CallOfferPayload (e.g., callId, conversationId, sdpRevision, deviceId). Without forwarding those fields, clients can’t reliably correlate offers when multiple calls/signaling sessions exist.

Copilot uses AI. Check for mistakes.
Comment on lines 72 to 81
socket.on(SocketEvents.CALL_ANSWER, ({ to, answer }: CallAnswerPayload) => {
io.to(to).emit(SocketEvents.CALL_ANSWER, {
from: socket.data.userId,
const from = socket.data.userId;
if (!to) return;

io.to(userRoom(to)).emit(SocketEvents.CALL_ANSWER, {
from,
to,
answer,
});
});
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CALL_ANSWER handler forwards only {from,to,answer} and drops CallAnswerPayload fields like callId/sdpRevision/deviceId. This makes the answer ambiguous in concurrent/overlapping calls and loses versioning hints; forward the relevant fields from the inbound payload.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +94
socket.on(SocketEvents.CALL_ICE_CANDIDATE, ({ to, candidate, callId }: CallIceCandidatePayload) => {
const from = socket.data.userId;
if (!to) return;

io.to(userRoom(to)).emit(SocketEvents.CALL_ICE_CANDIDATE, {
callId,
from,
to,
candidate,
});
});

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CALL_ICE_CANDIDATE relay drops sequencing/indexing fields from CallIceCandidatePayload (candidateSeq, mid, mLineIndex). If clients rely on ordering or specific m-line targeting, this data loss can break ICE replay/reconnect flows; forward these optional fields when present.

Suggested change
socket.on(SocketEvents.CALL_ICE_CANDIDATE, ({ to, candidate, callId }: CallIceCandidatePayload) => {
const from = socket.data.userId;
if (!to) return;
io.to(userRoom(to)).emit(SocketEvents.CALL_ICE_CANDIDATE, {
callId,
from,
to,
candidate,
});
});
socket.on(
SocketEvents.CALL_ICE_CANDIDATE,
({
to,
candidate,
callId,
candidateSeq,
mid,
mLineIndex,
}: CallIceCandidatePayload) => {
const from = socket.data.userId;
if (!to) return;
io.to(userRoom(to)).emit(SocketEvents.CALL_ICE_CANDIDATE, {
callId,
from,
to,
candidate,
candidateSeq,
mid,
mLineIndex,
});
}
);

Copilot uses AI. Check for mistakes.
Comment on lines 122 to 130
socket.on(SocketEvents.CALL_END, ({ to }: CallEndPayload) => {
io.to(to).emit(SocketEvents.CALL_END, {
from: socket.data.userId,
const from = socket.data.userId;
if (!to) return;

io.to(userRoom(to)).emit(SocketEvents.CALL_END, {
from,
to,
});
});
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CALL_END relay ignores optional CallEndPayload fields (callId, reason, endedAt) and emits only {from,to}. This prevents the receiver from validating which call ended and why; include the callId and propagate reason/timestamp when provided.

Copilot uses AI. Check for mistakes.
Comment on lines 132 to +140
socket.on(SocketEvents.CALL_BUSY, ({ to }: CallRingingPayload) => {
io.to(to).emit(SocketEvents.CALL_BUSY, {
from: socket.data.userId,
const from = socket.data.userId;
if (!to) return;

io.to(userRoom(to)).emit(SocketEvents.CALL_BUSY, {
from,
to,
});
});
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CALL_BUSY relay emits only {from,to} even though callId/conversationId can be present on CallRingingPayload. For correctness with overlapping calls, include callId (and conversationId if available) so the callee can associate the busy signal with the right call.

Copilot uses AI. Check for mistakes.
Comment on lines +142 to 151
socket.on(SocketEvents.CALL_RECONNECT, ({ to, callId }) => {
const from = socket.data.userId;
if (!to) return;

io.to(userRoom(to)).emit(SocketEvents.CALL_RECONNECT, {
callId,
from,
to,
});
});
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CALL_RECONNECT relay drops CallReconnectPayload fields like lastSeenSeq and iceRestartRequired. Those are typically needed to decide whether to resend buffered candidates/SDP or trigger an ICE restart; forward them (and validate callId) to support reconnection semantics.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +58
socket.on(
SocketEvents.CALL_OFFER_INIT,
({ callId, conversationId, to, callType }: CallOfferInitPayload) => {
const from = socket.data.userId;
if (!to) return;

io.to(userRoom(to)).emit(SocketEvents.CALL_RINGING, {
callId,
conversationId,
from,
to,
});

const statePayload: CallStatePayload = {
callId,
conversationId,
status: "ringing",
participants: [{ userId: from }, { userId: to }],
serverTs: new Date(),
};

io.to(userRoom(from)).emit(SocketEvents.CALL_STATE, statePayload);
io.to(userRoom(to)).emit(SocketEvents.CALL_STATE, statePayload);

io.to(userRoom(to)).emit(SocketEvents.CALL_OFFER_INIT, {
callId,
conversationId,
from,
to,
callType,
});
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CALL_OFFER_INIT currently emits both CALL_RINGING and CALL_OFFER_INIT to the callee with overlapping data, plus CALL_STATE to both sides. This duplication increases the chance of double-notifications and diverging client behavior; consider consolidating to a single “incoming call” event (or clearly separating responsibilities/payloads).

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +37
useEffect(() => {
const onRinging = (payload: CallOfferInitPayload) => {
setIncomingCall({
callId: payload.callId,
conversationId: payload.conversationId,
callType: payload.callType,
participants: [{ userId: payload.from }, { userId: payload.to }],
});
};
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The incoming-call handler is named onRinging but is wired to SocketEvents.CALL_OFFER_INIT (not CALL_RINGING). Given both events exist, this is easy to misread/maintain; either subscribe to CALL_RINGING (and use CallRingingPayload) or rename the handler/event usage to match intent.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +56
const onCallAccepted = () => {
stopRingtone();
setStatus("accepted");
};

const onCallRejected = () => {
stopRingtone();
setStatus("rejected");
};

const onCallEnded = () => {
endCall();
};
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CALL_ACCEPT / CALL_REJECT / CALL_END handlers update global call state without checking which call the event belongs to (payload includes callId). If a user can receive stale/overlapping events, this can incorrectly flip the UI state; gate the updates by comparing payload.callId to the current store callId.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +11
export enum CallState {
INITIATED = "initiated",
RINGING = "ringing",
ACCEPTED = "accepted",
ACTIVE = "active",
RECONNECTING = "reconnecting",
ENDED = "ended",
REJECTED = "rejected",
MISSED = "missed",
FAILED = "failed",
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packages/types defines a CallState string union, while packages/services exports a CallState enum with the same exported name but similar values. This naming collision makes cross-package imports ambiguous and error-prone; consider renaming the enum (e.g., CallLifecycleState) or re-exporting/deriving it from the shared @chat/types CallState.

Copilot uses AI. Check for mistakes.
harshlocham and others added 28 commits April 12, 2026 13:25
- Added new `RELEASES.md` file to document the release architecture and CI/CD workflow responsibilities.
- Updated `.changeset/config.json` to ignore specific packages during versioning.
- Enhanced `deploy.yml` to support manual promotion of releases with new inputs for `release_tag` and `deploy_target`.
- Modified `release.yml` to compute and output the short SHA of the release commit for better tracking.
- Updated `deploy.yml` to support manual promotion of releases with new inputs for `release_tag` and `deploy_target`.
- Improved deployment logic to handle both automatic and manual triggers, ensuring proper metadata resolution.
- Enhanced `RELEASES.md` to document the new manual promotion process and health check configurations.
- Added rollback triggers and notification features for production deployments.
- Removed debug logging from the socket handshake authentication process.
- Enhanced the `getClientSocketUrl` function to provide a default local socket URL and handle various local development scenarios.
- Updated the chat store to use a consistent `idOf` function for conversation identification, improving code clarity and maintainability.
- Added unit tests for the `getClientSocketUrl` function to ensure correct behavior in different environments.
- Added Prettier and related plugins to improve code formatting.
- Updated Next.js version in package.json and adjusted build commands in vercel.json for consistency.
- Refactored scripts in web's package.json for direct execution of Next.js commands.
- Cleaned up next.config.ts by removing unnecessary environment loading logic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants