feat: RTCRtpSender/Receiver.transport + selectedcandidatepairchange#47
feat: RTCRtpSender/Receiver.transport + selectedcandidatepairchange#47oliverlaz wants to merge 4 commits into
Conversation
…eTransport Expose the W3C transport chain sender.transport -> RTCDtlsTransport.iceTransport -> RTCIceTransport, surfacing the selectedcandidatepairchange event (plus statechange / gatheringstatechange). Stream always uses BUNDLE, so one ICE/DTLS transport pair is shared per RTCPeerConnection. The native binaries don't expose transport objects, so the transports are thin JS wrappers fed by a new peer-connection-level native event (peerConnectionSelectedCandidatePairChanged), sourced from iOS didChangeLocalCandidate:remoteCandidate:lastReceivedMs:changeReason: and Android PeerConnection.Observer.onSelectedCandidatePairChanged.
|
Warning Review limit reached
More reviews will be available in 40 minutes and 25 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. 📝 WalkthroughWalkthroughThis PR implements W3C-compliant ICE and DTLS transport abstractions for React Native WebRTC, exposing transport state and lifecycle through structured classes while wiring native candidate-pair-change events from Android and iOS into a unified JavaScript event system. ChangesICE and DTLS Transport State Management
Project Documentation
🎯 3 (Moderate) | ⏱️ ~25 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The shared RTCDtlsTransport/RTCIceTransport model assumes one transport per connection (max-bundle, which Stream uses). Warn once if more than one ICE transport name (candidate sdpMid / transport_name) is observed via selectedcandidatepairchange, and document the limitation.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/RTCPeerConnection.ts (1)
772-779:⚠️ Potential issue | 🟠 Major | ⚡ Quick winValidate native state values before mutating transport wrapper states.
evisanyhere, so unknown native values can leak invalid transport state (notably at Line 826 whereDTLS_TRANSPORT_STATE[ev.connectionState]can beundefined).
Please gate native states before calling_setState/_setGatheringStateand warn on unknown values.Suggested fix
addListener(this, 'peerConnectionIceConnectionChanged', (ev: any) => { @@ - this.iceConnectionState = ev.iceConnectionState; - this._iceTransport._setState(ev.iceConnectionState); + this.iceConnectionState = ev.iceConnectionState; + if ( + ev.iceConnectionState === 'new' || + ev.iceConnectionState === 'checking' || + ev.iceConnectionState === 'connected' || + ev.iceConnectionState === 'completed' || + ev.iceConnectionState === 'disconnected' || + ev.iceConnectionState === 'failed' || + ev.iceConnectionState === 'closed' + ) { + this._iceTransport._setState(ev.iceConnectionState); + } else { + log.warn(`${this._pcId} unexpected iceConnectionState: ${String(ev.iceConnectionState)}`); + } @@ addListener(this, 'peerConnectionStateChanged', (ev: any) => { @@ - this.connectionState = ev.connectionState; - this._dtlsTransport._setState(DTLS_TRANSPORT_STATE[ev.connectionState]); + this.connectionState = ev.connectionState; + const dtlsState = DTLS_TRANSPORT_STATE[ev.connectionState as RTCPeerConnectionState]; + if (dtlsState) { + this._dtlsTransport._setState(dtlsState); + } else { + log.warn(`${this._pcId} unexpected connectionState: ${String(ev.connectionState)}`); + } @@ addListener(this, 'peerConnectionIceGatheringChanged', (ev: any) => { @@ - this.iceGatheringState = ev.iceGatheringState; - this._iceTransport._setGatheringState(ev.iceGatheringState); + this.iceGatheringState = ev.iceGatheringState; + if ( + ev.iceGatheringState === 'new' || + ev.iceGatheringState === 'gathering' || + ev.iceGatheringState === 'complete' + ) { + this._iceTransport._setGatheringState(ev.iceGatheringState); + } else { + log.warn(`${this._pcId} unexpected iceGatheringState: ${String(ev.iceGatheringState)}`); + }Also applies to: 820-827, 914-921
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/RTCPeerConnection.ts` around lines 772 - 779, The callback added via addListener handling native events (the one that sets this.iceConnectionState and calls this._iceTransport._setState, plus similar handlers that call _setGatheringState and use DTLS_TRANSPORT_STATE[ev.connectionState]) must validate incoming native state values before mutating transport wrapper state: check that ev is defined and that ev.iceConnectionState / ev.connectionState / ev.gatheringState map to known enum values (e.g., membership in DTLS_TRANSPORT_STATE or the allowed ICE states) and only call this._iceTransport._setState or _setGatheringState when the mapped value is not undefined; if a value is unknown, log a warning identifying the event and the raw value instead of calling the setter. Ensure the same guarding logic is applied to the other similar handlers (the blocks around lines referenced and where DTLS_TRANSPORT_STATE[...] is used).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@android/src/main/java/com/oney/WebRTCModule/PeerConnectionObserver.java`:
- Around line 341-353: The Android observer currently forwards event.reason
directly (in onSelectedCandidatePairChanged) which sends null to JS; make it
consistent with iOS by normalizing a null reason to an empty string before
calling params.putString — inside the ThreadUtils.runOnExecutor lambda in
onSelectedCandidatePairChanged, replace the direct event.reason usage with a
normalized value (e.g., reason = event.reason == null ? "" : event.reason) and
pass that to params.putString so
webRTCModule.sendEvent("peerConnectionSelectedCandidatePairChanged", params)
behaves the same as the iOS implementation.
---
Outside diff comments:
In `@src/RTCPeerConnection.ts`:
- Around line 772-779: The callback added via addListener handling native events
(the one that sets this.iceConnectionState and calls
this._iceTransport._setState, plus similar handlers that call _setGatheringState
and use DTLS_TRANSPORT_STATE[ev.connectionState]) must validate incoming native
state values before mutating transport wrapper state: check that ev is defined
and that ev.iceConnectionState / ev.connectionState / ev.gatheringState map to
known enum values (e.g., membership in DTLS_TRANSPORT_STATE or the allowed ICE
states) and only call this._iceTransport._setState or _setGatheringState when
the mapped value is not undefined; if a value is unknown, log a warning
identifying the event and the raw value instead of calling the setter. Ensure
the same guarding logic is applied to the other similar handlers (the blocks
around lines referenced and where DTLS_TRANSPORT_STATE[...] is used).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8caa1b56-a551-4051-a876-31c45f1703ab
📒 Files selected for processing (12)
CLAUDE.mdandroid/src/main/java/com/oney/WebRTCModule/PeerConnectionObserver.javaios/RCTWebRTC/WebRTCModule+RTCPeerConnection.mios/RCTWebRTC/WebRTCModule.hios/RCTWebRTC/WebRTCModule.msrc/EventEmitter.tssrc/RTCDtlsTransport.tssrc/RTCIceTransport.tssrc/RTCPeerConnection.tssrc/RTCRtpReceiver.tssrc/RTCRtpSender.tssrc/index.ts
| @Override | ||
| public void onSelectedCandidatePairChanged(CandidatePairChangeEvent event) { | ||
| ThreadUtils.runOnExecutor(() -> { | ||
| WritableMap params = Arguments.createMap(); | ||
| params.putInt("pcId", id); | ||
| params.putMap("local", serializeIceCandidate(event.local)); | ||
| params.putMap("remote", serializeIceCandidate(event.remote)); | ||
| params.putInt("lastDataReceivedMs", event.lastDataReceivedMs); | ||
| params.putString("reason", event.reason); | ||
|
|
||
| webRTCModule.sendEvent("peerConnectionSelectedCandidatePairChanged", params); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Cross-platform reason field inconsistency.
Both implementations handle the changeReason/reason parameter differently when it is null:
- Android (PeerConnectionObserver.java:349): Passes
event.reasondirectly viaputString, which sendsnullto JavaScript when the native value is null. - iOS (WebRTCModule+RTCPeerConnection.m:922): Uses
reason ?: @"", convertingnilto an empty string before sending to JavaScript.
JavaScript consumers will receive null on Android and "" on iOS for the same missing-reason scenario. For consistent cross-platform behavior, both should either pass null or use the same fallback string.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@android/src/main/java/com/oney/WebRTCModule/PeerConnectionObserver.java`
around lines 341 - 353, The Android observer currently forwards event.reason
directly (in onSelectedCandidatePairChanged) which sends null to JS; make it
consistent with iOS by normalizing a null reason to an empty string before
calling params.putString — inside the ThreadUtils.runOnExecutor lambda in
onSelectedCandidatePairChanged, replace the direct event.reason usage with a
normalized value (e.g., reason = event.reason == null ? "" : event.reason) and
pass that to params.putString so
webRTCModule.sendEvent("peerConnectionSelectedCandidatePairChanged", params)
behaves the same as the iOS implementation.
What
Adds a partial implementation of the W3C transport chain so consumers can observe ICE candidate-pair changes:
New surface:
RTCRtpSender.transport/RTCRtpReceiver.transport→RTCDtlsTransportRTCDtlsTransport:iceTransport,state,statechangeeventRTCIceTransport:state,gatheringState,getSelectedCandidatePair(), andselectedcandidatepairchange/statechange/gatheringstatechangeeventsWhy / design
The end goal is the
selectedcandidatepairchangeevent. The underlying WebRTC binaries (iOSStreamWebRTC, Androidstream-video-webrtc-android, both 145.x) do not exposeRTCDtlsTransport/RTCIceTransportobjects or asender.transportgetter — so a faithful native-backed implementation isn't possible without patching the binaries.However, the candidate-pair change data is available at the peer-connection level on both platforms. And because Stream always uses BUNDLE, there is exactly one ICE/DTLS transport per
RTCPeerConnection. So:RTCPeerConnectionowns a singleRTCDtlsTransport+RTCIceTransportpair, injected into every sender/receiver viatransport.peerConnectionSelectedCandidatePairChangedfeeds the ICE transport, sourced from:RTCPeerConnectionDelegatedidChangeLocalCandidate:remoteCandidate:lastReceivedMs:changeReason:PeerConnection.Observer.onSelectedCandidatePairChanged(CandidatePairChangeEvent)state/gatheringStateare best-effort, derived from the connection'siceConnectionState/iceGatheringState/connectionState, and fire their corresponding change events on transition.Known limitation: only the single bundled transport is modeled. If bundling is disabled (multiple m-line transports), all senders/receivers still report the one shared transport. Acceptable — Stream always bundles.
Verification
npm run lint(eslint --max-warnings 0+tsc --noEmit) passes — the JS CI gate.google-webrtc/webrtc-iosrefs).npm install+pod installrequired) — reviewers/CI should confirm the iOS/Android example builds compile.Also includes the project
CLAUDE.md(separatedocs:commit).Summary by CodeRabbit
Release Notes
New Features
RTCDtlsTransportandRTCIceTransportclasses for managing media transport connections.Documentation