feat: RTCRtpSender/Receiver.transport + selectedcandidatepairchange#47
feat: RTCRtpSender/Receiver.transport + selectedcandidatepairchange#47oliverlaz wants to merge 6 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 17 minutes and 11 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
📝 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. Configuration and build tooling are updated accordingly, and comprehensive project documentation guides future development. ChangesICE and DTLS Transport State Management
Documentation and Build Configuration
🎯 3 (Moderate) | ⏱️ ~28 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.
getSelectedCandidatePair() must return either null or a pair with both members populated, and selectedcandidatepairchange must fire only on an actual pair change. Native can emit a transient event with one side unresolved, and re-emit the same pair as lastDataReceivedMs ticks, so: - drop events where either candidate is null (never expose, or clobber an existing pair with, a half-populated pair) - de-dupe against the current pair so the event fires only on real changes - tighten RTCIceCandidatePair members to non-nullable RTCIceCandidate Also bump build tooling/deps: react-native-builder-bob 0.18.2 -> ^0.43.0, debug 4.3.4 -> ^4.4.3, base64-js -> ^1.5.1, tsconfig target/lib es6 -> es2022, with the regenerated lockfile.
The shared ICE/DTLS transport pair faithfully represents a connection only when it negotiates a single transport, i.e. bundlePolicy: 'max-bundle'. Previously the transport was created for every connection and a runtime warning fired only *after* the shared selected-pair had already been overwritten, so on a non-max-bundle connection every sender/receiver's transport.iceTransport could report a candidate pair belonging to another path. Gate exposure structurally instead of warning: create _iceTransport / _dtlsTransport only when bundlePolicy is 'max-bundle'. Otherwise leave them null, so sender/receiver `transport` returns null (spec-legal) rather than a possibly-wrong transport. Drop the now-redundant _seenTransportNames / _multiTransportWarned detection and update the transport docs accordingly.
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, including state and event support (statechange, ICE gathering, and selected-candidate-pair changes).peerConnectionSelectedCandidatePairChangedsupport to reflect the active ICE candidate pair.RTCRtpSenderandRTCRtpReceivernow expose their associated DTLS transport via atransportgetter.Documentation
CLAUDE.mdwith repo guidance, architecture overview, build/release instructions, and verification steps for iOS/Android.