-
Notifications
You must be signed in to change notification settings - Fork 13k
chore: Implement base signaling system for media calls #36452
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Looks like this PR is ready to merge! 🎉 |
|
ab771e1 to
4d4b53c
Compare
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #36452 +/- ##
===========================================
- Coverage 66.36% 66.33% -0.03%
===========================================
Files 3390 3390
Lines 115279 115282 +3
Branches 21108 21107 -1
===========================================
- Hits 76501 76474 -27
- Misses 36172 36204 +32
+ Partials 2606 2604 -2
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
packages/media-signaling/src/lib/services/webrtc/LocalStream.ts (1)
13-16: Guard for non‑audio tracks is in place — good.Matches earlier feedback and prevents misrouting video to audio sender.
🧹 Nitpick comments (7)
packages/media-signaling/src/lib/services/webrtc/LocalStream.ts (3)
27-29: SurfacereplaceTrackfailures and include track id in logs.Catch and log errors to avoid unhandled rejections; add context to debug logs.
- this.logger?.debug('LocalStream.setRemoteTrack'); - await this.audioTransceiver.sender.replaceTrack(newTrack); + this.logger?.debug('LocalStream.setRemoteTrack', newTrack ? newTrack.id : null); + try { + await this.audioTransceiver.sender.replaceTrack(newTrack); + } catch (err) { + this.logger?.error('LocalStream.replaceTrack failed', err); + throw err; + }
14-16: Remove redundant optional chaining.
newTrackis already truthy;?.is unnecessary.- if (newTrack && newTrack?.kind !== 'audio') { + if (newTrack && newTrack.kind !== 'audio') {
10-10: Considersendonlyfor the local audio transceiver.If receiving is handled elsewhere (e.g., a dedicated RemoteStream), prefer
sendonlyto avoid accidental coupling.- this.audioTransceiver = this.peer.addTransceiver('audio', { direction: 'sendrecv' }); + this.audioTransceiver = this.peer.addTransceiver('audio', { direction: 'sendonly' });packages/media-signaling/src/lib/services/webrtc/Stream.ts (4)
30-32: Release mic resources on stop.Stopping tracks before removal avoids lingering device capture and improves UX/privacy.
public stopAudio(): void { - this.removeAudioTracks(); + // Stop local audio tracks to release the microphone, then remove them + this.mediaStream.getAudioTracks().forEach((track) => { + try { + track.stop(); + } catch {} + }); + this.removeAudioTracks(); }
34-43: Drop impossible null checks intoggleAudioTracks.
getAudioTracks()yieldsMediaStreamTrack[]; the!trackguard is dead code.protected toggleAudioTracks(): void { this.logger?.debug('Stream.toggleAudioTracks', this.enabled); - this.mediaStream.getAudioTracks().forEach((track) => { - if (!track) { - return; - } - - track.enabled = this.enabled; - }); + this.mediaStream.getAudioTracks().forEach((track) => { + track.enabled = this.enabled; + }); }
45-54: Same here: remove redundant null guard inremoveAudioTracks.protected removeAudioTracks(): void { this.logger?.debug('Stream.removeAudioTracks'); - this.mediaStream.getAudioTracks().forEach((track) => { - if (!track) { - return; - } - - this.mediaStream.removeTrack(track); - }); + this.mediaStream.getAudioTracks().forEach((track) => { + this.mediaStream.removeTrack(track); + }); }
56-73: ClarifysetAudioTrackreturn semantics.A boolean conflates “invalid kind” vs “already present”. Consider a discriminated union for clearer call‑site logic. Non‑blocking.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
packages/media-signaling/src/lib/Session.ts(1 hunks)packages/media-signaling/src/lib/services/webrtc/LocalStream.ts(1 hunks)packages/media-signaling/src/lib/services/webrtc/Processor.ts(1 hunks)packages/media-signaling/src/lib/services/webrtc/RemoteStream.ts(1 hunks)packages/media-signaling/src/lib/services/webrtc/Stream.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/media-signaling/src/lib/services/webrtc/RemoteStream.ts
- packages/media-signaling/src/lib/services/webrtc/Processor.ts
- packages/media-signaling/src/lib/Session.ts
🧰 Additional context used
🧬 Code graph analysis (2)
packages/media-signaling/src/lib/services/webrtc/LocalStream.ts (2)
packages/media-signaling/src/lib/services/webrtc/Stream.ts (1)
Stream(3-74)packages/media-signaling/src/definition/logger.ts (1)
IMediaSignalLogger(1-6)
packages/media-signaling/src/lib/services/webrtc/Stream.ts (1)
packages/media-signaling/src/definition/logger.ts (1)
IMediaSignalLogger(1-6)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: CodeQL-Build
- GitHub Check: CodeQL-Build
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (5)
packages/media-signaling/src/lib/services/webrtc/Processor.ts (4)
318-320: Sanitize ICE candidate logging and avoid counting null end-of-candidates.Avoid logging IPs/ports; also don’t increment on null candidates.
Apply this diff:
- this.config.logger?.debug('MediaCallWebRTCProcessor.onIceCandidate', event.candidate); - this.iceCandidateCount++; + const c = event.candidate; + this.config.logger?.debug('MediaCallWebRTCProcessor.onIceCandidate', c ? { protocol: c.protocol, sdpMLineIndex: c.sdpMLineIndex } : null); + if (c) { + this.iceCandidateCount++; + }
100-107: Reset ICE wait state on non-restart offers too.Without
startNewNegotiation()wheniceRestartis false,iceGatheringFinishedmay remain true and skip waiting for ICE.Apply this diff:
if (iceRestart) { this.restartIce(); + } else { + this.startNewNegotiation(); } const offer = await this.peer.createOffer();
222-235: Don’t fail on ICE timeout; proceed with available candidates.Let offers/answers continue with partial ICE; surface timeout via state instead of throwing.
Apply this diff:
- await this.waitForIceGathering(); + try { + await this.waitForIceGathering(); + } catch (e) { + if ((e as Error).message !== 'timeout') { + throw e; + } + // Continue with partial/no candidates; state already reflects timeout. + }
396-400: Initialize flag only after successful track setup.Setting
localMediaStreamInitialized = truebeforeawait setTrack(...)can leave a false-positive initialized state if it throws.Apply this diff:
private async loadInputTrack(): Promise<void> { this.config.logger?.debug('MediaCallWebRTCProcessor.loadInputTrack'); - this.localMediaStreamInitialized = true; - await this.localStream.setTrack(this.inputTrack); + await this.localStream.setTrack(this.inputTrack); + this.localMediaStreamInitialized = true; }packages/media-signaling/src/lib/Call.ts (1)
671-676: Emit the new client state (not the old one).Event consumers expect the current state; emitting
oldClientStateis misleading.Apply this diff:
this.updateStateTimeouts(); this.requestStateReport(); this.oldClientState = clientState; - this.emitter.emit('clientStateChange', oldClientState); + this.emitter.emit('clientStateChange', clientState);
🧹 Nitpick comments (4)
packages/media-signaling/src/lib/services/webrtc/LocalStream.ts (1)
27-42: Stabilize audio sender lookup across replaceTrack(null) to avoid extra transceivers.After you start detaching with
replaceTrack(null),peer.getSenders().find(s => s.track?.kind === 'audio')won’t find the sender and you’ll create new transceivers. Persist and reuse the known audio transceiver/sender.Within this method, prefer the known sender and fall back to discovery; persist the transceiver when first created:
- const sender = this.peer.getSenders().find((sender) => sender.track?.kind === 'audio'); + const knownSender = this.transceiver?.sender ?? null; + let sender = knownSender ?? this.peer.getSenders().find((s) => s.track?.kind === 'audio') ?? null; if (!sender) { if (newTrack) { this.logger?.debug('LocalStream.setRemoteTrack.addTrack'); // This will require a re-negotiation - this.peer.addTrack(newTrack, this.mediaStream); + const added = this.peer.addTrack(newTrack, this.mediaStream); + this.transceiver = this.peer.getTransceivers().find((t) => t.sender === added) ?? this.transceiver; } return; } // If the peer already has a track of the same kind, we can just replace it with the new track with no issues await sender.replaceTrack(newTrack); + this.transceiver ??= this.peer.getTransceivers().find((t) => t.sender === sender) ?? null;And add this field to the class:
private transceiver: RTCRtpTransceiver | null = null;packages/media-signaling/src/lib/Call.ts (3)
709-712: Clarify targeting vs signature error in logs.This branch fires when the signal isn’t targeted to this session; it’s not an “unsigned offer request”. Downgrade to debug or fix the message.
- this.config.logger?.error('Received an unsigned offer request.'); + this.config.logger?.debug('Offer request not targeted to this session. Ignoring.');
1100-1106: Verify contract/session id used for signal targeting.Server signals use
toContractId. This compares againstconfig.sessionId, but Transport usestransporter.contractId. Mismatch will drop valid signals.Proposed change:
- if (signal.toContractId) { - return signal.toContractId === this.config.sessionId; - } + if (signal.toContractId) { + return signal.toContractId === this.config.transporter.contractId; + }If
sessionIdis indeed the same value, please confirm and add a doc comment tying both together.
645-647: Confirm event payload semantics for stateChange.Docs say “with the old state as param”. If that’s intentional, fine; otherwise switch to new state for consistency with
clientStateChange.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
packages/media-signaling/src/lib/Call.ts(1 hunks)packages/media-signaling/src/lib/Session.ts(1 hunks)packages/media-signaling/src/lib/services/webrtc/LocalStream.ts(1 hunks)packages/media-signaling/src/lib/services/webrtc/Processor.ts(1 hunks)packages/media-signaling/src/lib/services/webrtc/Stream.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/media-signaling/src/lib/services/webrtc/Stream.ts
- packages/media-signaling/src/lib/Session.ts
🧰 Additional context used
🧬 Code graph analysis (3)
packages/media-signaling/src/lib/services/webrtc/LocalStream.ts (1)
packages/media-signaling/src/lib/services/webrtc/Stream.ts (1)
Stream(3-72)
packages/media-signaling/src/lib/services/webrtc/Processor.ts (6)
packages/media-signaling/src/definition/services/webrtc/IWebRTCProcessor.ts (4)
IWebRTCProcessor(17-34)WebRTCProcessorEvents(15-15)WebRTCProcessorConfig(36-42)WebRTCInternalStateMap(7-13)packages/media-signaling/src/lib/services/webrtc/LocalStream.ts (1)
LocalStream(3-43)packages/media-signaling/src/lib/services/webrtc/RemoteStream.ts (1)
RemoteStream(3-7)packages/media-signaling/src/lib/utils/getExternalWaiter.ts (2)
PromiseWaiterData(1-7)getExternalWaiter(15-70)packages/media-signaling/src/lib/Call.ts (2)
muted(95-101)held(104-110)packages/media-signaling/src/definition/services/IServiceProcessor.ts (1)
ServiceStateValue(9-12)
packages/media-signaling/src/lib/Call.ts (12)
packages/media-signaling/src/definition/logger.ts (1)
IMediaSignalLogger(1-6)packages/media-signaling/src/lib/TransportWrapper.ts (2)
MediaSignalTransportWrapper(12-53)answer(37-39)packages/media-signaling/src/definition/services/IServiceProcessorFactoryList.ts (1)
IServiceProcessorFactoryList(3-5)packages/media-signaling/src/definition/client.ts (2)
ClientState(1-13)ClientContractState(15-20)packages/media-signaling/src/definition/call/CallEvents.ts (1)
CallEvents(4-28)packages/media-signaling/src/definition/services/webrtc/IWebRTCProcessor.ts (2)
IWebRTCProcessor(17-34)WebRTCInternalStateMap(7-13)packages/media-signaling/src/definition/signals/server/MediaSignal.ts (1)
ServerMediaSignal(7-12)packages/media-signaling/src/definition/signals/server/remote-sdp.ts (1)
ServerMediaSignalRemoteSDP(2-9)packages/media-signaling/src/definition/signals/server/new.ts (1)
ServerMediaSignalNewCall(4-18)packages/media-signaling/src/lib/services/states.ts (1)
isPendingState(4-6)packages/media-signaling/src/definition/signals/server/request-offer.ts (1)
ServerMediaSignalRequestOffer(2-8)packages/media-signaling/src/definition/signals/client/error.ts (1)
ClientMediaSignalError(4-12)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: 📦 Build Packages
- GitHub Check: CodeQL-Build
- GitHub Check: CodeQL-Build
- GitHub Check: CodeQL-Build
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
packages/models/src/models/MediaCalls.ts (1)
40-53: Caller-scoped lookup fixes the prior auth confusion.Looks good and addresses the earlier $or bypass risk by scoping to caller.
🧹 Nitpick comments (6)
packages/models/src/models/MediaCalls.ts (6)
29-37: Add an index for expiry scans; consider uniqueness for callerRequestedId.
- findAllExpiredCalls filters by ended + expiresAt but current compound index is ended,uids,expiresAt. Without uids equality, the query won’t leverage the expiresAt range. Add a supporting index.
- If semantics require a single callerRequestedId per caller, make that composite sparse index unique to prevent ambiguous matches.
Apply:
{ - key: { 'caller.type': 1, 'caller.id': 1, 'callerRequestedId': 1 }, - sparse: true, + key: { 'caller.type': 1, 'caller.id': 1, 'callerRequestedId': 1 }, + unique: true, // enforce one requestedId per caller + sparse: true, }, + // Supports ended + expiresAt range scans used by findAllExpiredCalls + { key: { ended: 1, expiresAt: 1 }, unique: false },If uniqueness is not desired, omit the unique flag; otherwise confirm no existing duplicates before rollout.
63-71: Gate ringing transition on not-ended.Prevent state changes on already-ended calls.
return this.updateOne( { _id: callId, - state: 'none', + state: 'none', + ended: false, }, { $set: { state: 'ringing', expiresAt } }, );
73-89: Accept transition should also ensure not-ended; verify callee.contractId schema.
- Add ended: false to the filter for robustness.
- Confirm MediaCallContactInformation defines contractId to avoid schema drift.
{ _id: callId, state: { $in: ['none', 'ringing'] }, + ended: false, }, { $set: { - 'state': 'accepted', + state: 'accepted', 'callee.contractId': calleeContractId, expiresAt, }, },
91-104: Activate transition should ensure not-ended.Add ended: false to avoid re-activating a call that raced to hangup/expire.
{ _id: callId, state: 'accepted', + ended: false, },
138-157: Block transfers on ended calls.Guard on ended: false alongside state and transferredAt checks.
{ _id: callId, - state: { - $in: ['accepted', 'active'], - }, - transferredAt: { - $exists: false, - }, + state: { $in: ['accepted', 'active'] }, + ended: false, + transferredAt: { $exists: false }, },
159-169: This query will benefit from ended+expiresAt index.As written, it can’t use the expiresAt range efficiently with the current ended,uids,expiresAt index. The additional { ended: 1, expiresAt: 1 } index proposed above will optimize this path.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
ee/packages/media-calls/src/internal/InternalCallProvider.ts(1 hunks)packages/model-typings/src/models/IMediaCallsModel.ts(1 hunks)packages/models/src/models/MediaCalls.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- ee/packages/media-calls/src/internal/InternalCallProvider.ts
- packages/model-typings/src/models/IMediaCallsModel.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/models/src/models/MediaCalls.ts (3)
packages/core-typings/src/mediaCalls/IMediaCall.ts (4)
IMediaCall(35-63)MediaCallActorType(5-5)MediaCallSignedActor(15-15)MediaCallContact(28-28)packages/model-typings/src/models/IMediaCallsModel.ts (1)
IMediaCallsModel(6-22)packages/core-typings/src/IUser.ts (1)
IUser(186-252)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: 📦 Build Packages
- GitHub Check: CodeQL-Build
- GitHub Check: CodeQL-Build
- GitHub Check: CodeQL-Build
🔇 Additional comments (1)
packages/models/src/models/MediaCalls.ts (1)
24-27: LGTM on scaffolding and straightforward helpers.
- Class/constructor and updateOneById are clean.
- hangupCallById and setExpiresAtById guard on ended as expected.
- findAllNotOverByUid and the boolean checks use efficient filters with count limit 1.
Also applies to: 55-61, 106-136, 171-199
tassoevan
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
KevLehman
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still finishing, halfway done lol

Proposed changes (including videos or screenshots)
This PR implements the base signaling system for media calls, along with a basic webrtc implementation. It doesn't integrate this new system with the rocket.chat UI so it's not usable by itself. Changes are still expected on this codebase as we implement more features and add SIP integration.
Issue(s)
VAI-14
VAI-83
Steps to test or reproduce
Further comments
This PR introduces two new packages, and modifies a few others:
media-signaling:
This package includes all of the business logic that clients need to implement in order to receive calls. It also exports all the types that are shared between client and server. It is meant to be published to npm, so it doesn't have any dependency on other packages from the workspace. That way any client that wants to implement media calls can import this package to get the full functionality while having only to implement the UI for it.
media-calls:
This package implements the server logic for the whole media call system, without integrating it with rocket.chat itself (aka this package simply being there won't do anything at all). It imports things from other packages when it made sense to implement them on existing packages (like models and some core typings), but otherwise is mostly self-contained.
other modified packages:
Some existing packages such as core-services and ddp-client (as well as the main meteor app) were modified to actually connect the rocket.chat core with the media-calls package. In general those do not include any business logic for the media call system.
Summary by CodeRabbit
New Features
Bug Fixes
Chores