fix(endpoint): route RTX/FEC repair packets to primary stream receiver#75
fix(endpoint): route RTX/FEC repair packets to primary stream receiver#75nightness wants to merge 10 commits into
Conversation
9b00c53 to
71e8f84
Compare
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Fixes routing and identification of RTX/FEC repair RTP packets so they reach the primary stream receiver instead of being dropped.
Changes:
- Extend SSRC matching to include RTX/FEC repair SSRCs and early-route them to the primary receiver without track-open side effects.
- Fix
rridhandling to return the base track immediately after registering the repair SSRC. - Prevent RTX codecs from being selected as the primary codec in the undeclared-SSRC path.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // For primary streams, look up the primary codec only (not RTX/FEC codecs). | ||
| // RTX/FEC packets are routed via the early-return above. | ||
| let track_codec = if is_track_codec_empty | ||
| && let Some(rtp_header) = rtp_header | ||
| && let Some(codec) = receiver | ||
| .get_codec_preferences() | ||
| .iter() | ||
| .find(|codec| codec.payload_type == rtp_header.payload_type) | ||
| //TODO: what about RTX/FEC stream? | ||
| { | ||
| Some(codec.rtp_codec.clone()) | ||
| } else { |
There was a problem hiding this comment.
The comment says this lookup is 'primary codec only (not RTX/FEC codecs)', but the predicate currently matches only payload_type and does not exclude RTX (and potentially other repair-related codecs). Either (a) update the predicate to actually exclude repair codecs (consistent with the later change at line ~570), or (b) adjust the comment to reflect the actual behavior/assumptions of this branch.
There was a problem hiding this comment.
Addressed: the predicate now includes && !is_repair_mime_type(...) which excludes all repair codecs (video/rtx, audio/rtx, ulpfec, flexfec, flexfec-03). This comment should be marked outdated — the fix is on adjacent lines.
| .find(|codec| { | ||
| codec.payload_type == rtp_header.payload_type | ||
| && !codec | ||
| .rtp_codec | ||
| .mime_type | ||
| .eq_ignore_ascii_case(MIME_TYPE_RTX) | ||
| }) |
There was a problem hiding this comment.
The RTX exclusion is an exact match against a single MIME_TYPE_RTX string. In SDP/RTP ecosystems RTX commonly appears as video/rtx and can also appear as audio/rtx; if MIME_TYPE_RTX is only one of these, the other will not be filtered and could still be incorrectly selected as the primary codec. Consider excluding RTX by a more general check (e.g., case-insensitive suffix /rtx or a shared is_rtx_mime_type helper) to ensure all RTX mime-type variants are rejected here.
There was a problem hiding this comment.
Addressed: replaced the single MIME_TYPE_RTX check with is_repair_mime_type() which case-insensitively matches all repair MIME types across any media type (video/rtx, audio/rtx, ulpfec, flexfec, flexfec-03). This comment should be marked outdated.
- Fix import order (MIME_TYPE_RTX, MediaEngine) to pass cargo fmt - Add missing RTCRtpRtxParameters import (was causing compilation error) - Eliminate redundant receiver()/get_coding_parameters() lookups by using find_map to classify SSRC as primary vs repair in a single pass - Fix comment/predicate mismatch: primary codec lookup now actually excludes repair codecs (RTX and FEC) via is_repair_mime_type helper - Replace hardcoded MIME_TYPE_RTX check with general is_repair_mime_type that covers all RTX variants (video/rtx, audio/rtx) and FEC types (ulpfec, flexfec, flexfec-03) case-insensitively - Add regression tests for is_repair_mime_type covering RTX variants, FEC variants, and primary codec rejection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Determine whether the SSRC matches a primary or repair (RTX/FEC) sub-stream | ||
| // in a single pass, avoiding redundant receiver/coding-parameter lookups. | ||
| // | ||
| // First, classify the SSRC across all transceivers. We record whether the match | ||
| // was against a primary SSRC or a repair (RTX/FEC) SSRC so the subsequent code | ||
| // can take the right path without re-scanning coding parameters. | ||
| let ssrc_match = self | ||
| .rtp_transceivers | ||
| .iter() | ||
| .enumerate() | ||
| .find_map(|(id, transceiver)| { | ||
| let receiver = transceiver.receiver().as_ref()?; | ||
| let mut is_repair = false; | ||
| let matched = receiver.get_coding_parameters().iter().any(|coding| { | ||
| if coding.ssrc.is_some_and(|coding_ssrc| coding_ssrc == ssrc) { | ||
| return true; | ||
| } | ||
| }) | ||
| { | ||
| // Also match RTX/FEC repair SSRCs so repair packets are routed to | ||
| // the primary stream's receiver rather than silently dropped. | ||
| if coding.rtx.as_ref().is_some_and(|r| r.ssrc == ssrc) | ||
| || coding.fec.as_ref().is_some_and(|f| f.ssrc == ssrc) | ||
| { | ||
| is_repair = true; | ||
| return true; | ||
| } | ||
| false | ||
| }); | ||
| if matched { | ||
| // Grab the track_id while we have the receiver borrowed immutably, | ||
| // so repair packets can be returned without a second lookup. | ||
| let track_id = receiver.track().track_id().clone(); | ||
| Some((id, is_repair, track_id)) | ||
| } else { | ||
| None | ||
| } | ||
| }); | ||
|
|
||
| if let Some((id, is_repair, repair_track_id)) = ssrc_match { | ||
| // If the SSRC belongs to a repair (RTX/FEC) sub-stream, route it to the primary | ||
| // stream's receiver without firing track-open events or updating codec state. | ||
| if is_repair { | ||
| return Some(repair_track_id); | ||
| } |
There was a problem hiding this comment.
New routing behavior for repair SSRCs (RTX/FEC) is introduced here, but the added unit tests only cover is_repair_mime_type. Consider adding regression tests that exercise find_track_id_by_ssrc/find_track_id_by_rid with an RTX/FEC SSRC to ensure the packet is routed to the primary track_id and that no RTCTrackEvent::OnOpen is emitted for the repair SSRC.
There was a problem hiding this comment.
Addressed: 7 regression tests added covering find_track_id_by_ssrc with RTX, FEC, primary SSRCs, unknown SSRCs, and rrid stats registration. This comment should be marked outdated — the tests are in the same file.
| // rrid identifies the base stream (rid) that this repair/RTX packet belongs to. | ||
| // Associate the repair SSRC with the base stream's RTX parameters, then route | ||
| // the packet to the primary stream's receiver (not a new track). | ||
| if let Some(coding) = receiver.get_coding_parameter_mut_by_rid(rrid.as_str()) { | ||
| match coding.rtx.as_mut() { | ||
| Some(rtx) => rtx.ssrc = ssrc, | ||
| None => coding.rtx = Some(RTCRtpRtxParameters { ssrc }), | ||
| } | ||
| } | ||
| return Some(receiver.track().track_id().clone()); |
There was a problem hiding this comment.
When rrid packets arrive, this updates coding.rtx.ssrc but does not update the stats accumulator’s RTX↔primary reverse lookup (populated in RTCStatsAccumulator::get_or_create_inbound_rtp_streams). If the primary stream’s stats entry was already created before rrid is seen, on_rtx_packet_received_if_rtx() will never recognize the new RTX SSRC, so RTX receive stats won’t be tracked. Consider updating the stats mapping (and, if applicable, the existing inbound stream’s rtx_ssrc field) when registering the RTX SSRC here.
There was a problem hiding this comment.
Addressed: added register_rtx_ssrc(rtx_ssrc, primary_ssrc) to RTCStatsAccumulator, called from the rrid branch. Updates both the reverse lookup map and the inbound stream's rtx_ssrc field. This comment should be marked outdated.
Address remaining Copilot review comments on PR webrtc-rs#75: - Fix stats gap for rrid packets: when an RTX SSRC is learned via the rrid header extension after the primary stream's stats entry exists, register it in the accumulator's reverse lookup map so on_rtx_packet_received_if_rtx() can track retransmission stats. - Add register_rtx_ssrc() to RTCStatsAccumulator for late-discovered RTX SSRCs. - Add 7 regression tests covering: - RTX SSRC routed to primary track via find_track_id_by_ssrc - FEC SSRC routed to primary track via find_track_id_by_ssrc - No OnOpen event emitted for repair SSRC routing - Primary SSRC still routes correctly - Unknown SSRC returns None - rrid branch registers RTX SSRC in stats accumulator - handle_undeclared_ssrc rejects RTX codec as primary Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
webrtc-rs#12) Implements rrid (urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id) handling, resolving webrtc-rs#12. rrid is already parsed from RTP header extensions and the URI is registered — the handler body was a TODO stub. When an RTP packet carries rrid, it is a repair/RTX stream for the base stream identified by that rrid value (= the base stream's rid). Map the repair SSRC into the base stream's coding parameters' RTX field so downstream routing and stats can correlate the repair stream with its source stream. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Storing the RTX SSRC in coding parameters alone was insufficient — the interceptor also needs to know about the repair stream so RTX packets are actually demuxed and forwarded. This mirrors the existing RTX handling in interceptor_remote_streams_op. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
If the base stream's coding parameters don't exist yet when a repair/RTX packet arrives, the SSRC association is silently dropped. Add a warn! log so this failure mode is observable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Exercises the rrid code path in endpoint.rs::find_track_id_by_rid(): - Sends base RTP packets with rid extension for 3 simulcast layers - Sends RTX packets with rrid extension (different SSRC, RTX payload type) - Asserts exactly 3 tracks are created (no spurious tracks for RTX SSRCs) This verifies the RTX SSRC→base stream association and interceptor registration work correctly through the full pipeline. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Guard interceptor registration behind has_base_coding: skip binding repair stream when no base coding parameters exist for the rrid, preventing invalid/unknown rrid values from creating orphan remote streams - Fix RTX payload type lookup: use codec.payload_type directly since the packet is already an RTX packet (find_rtx_payload_type was searching for apt=<rtx_pt> which never matches) - Fix test poll_read loop: drain all message types to prevent stalling on non-RTP messages (e.g. RTCP) - Remove unused import (RTCIceConnectionState) and find_rtx_payload_type Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add `update_inbound_rtx_ssrc` to RTCStatsAccumulator so rrid-discovered RTX SSRCs are reflected in stats (rtx_ssrc_to_primary map + inbound stream's rtx_ssrc field) - Return `Some(track_id)` from the rrid branch in find_track_id_by_rid so RTX packets are routed to the correct receiver instead of dropped - Add unit tests for update_inbound_rtx_ssrc (with and without existing inbound stream) - Fix clippy warnings in test and stats_tests - Add note in integration test explaining write_rtp limitation for RTX Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs caused all RTX/FEC repair packets to be silently dropped: 1. find_track_id_by_ssrc: outer SSRC search only matched primary SSRCs (coding.ssrc). RTX/FEC SSRCs live in coding.rtx.ssrc / coding.fec.ssrc and were never found. Fix: extend the .any() predicate to also check rtx/fec SSRCs; when matched via repair SSRC, return the primary stream's track_id immediately without triggering OnOpen events. 2. find_track_id_by_rid (rrid branch): after correctly associating the repair SSRC with the base stream's RTX parameters, the function fell through to None instead of returning Some(track_id). RTX packets with the rrid header extension were always dropped on every packet. 3. handle_undeclared_ssrc: codec lookup could match the RTX codec entry (which IS in codec_preferences) and incorrectly set up a repair stream as a primary track. Fix: explicitly exclude RTX codecs so only primary codecs are accepted here. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix import order (MIME_TYPE_RTX, MediaEngine) to pass cargo fmt - Add missing RTCRtpRtxParameters import (was causing compilation error) - Eliminate redundant receiver()/get_coding_parameters() lookups by using find_map to classify SSRC as primary vs repair in a single pass - Fix comment/predicate mismatch: primary codec lookup now actually excludes repair codecs (RTX and FEC) via is_repair_mime_type helper - Replace hardcoded MIME_TYPE_RTX check with general is_repair_mime_type that covers all RTX variants (video/rtx, audio/rtx) and FEC types (ulpfec, flexfec, flexfec-03) case-insensitively - Add regression tests for is_repair_mime_type covering RTX variants, FEC variants, and primary codec rejection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address remaining Copilot review comments on PR webrtc-rs#75: - Fix stats gap for rrid packets: when an RTX SSRC is learned via the rrid header extension after the primary stream's stats entry exists, register it in the accumulator's reverse lookup map so on_rtx_packet_received_if_rtx() can track retransmission stats. - Add register_rtx_ssrc() to RTCStatsAccumulator for late-discovered RTX SSRCs. - Add 7 regression tests covering: - RTX SSRC routed to primary track via find_track_id_by_ssrc - FEC SSRC routed to primary track via find_track_id_by_ssrc - No OnOpen event emitted for repair SSRC routing - Primary SSRC still routes correctly - Unknown SSRC returns None - rrid branch registers RTX SSRC in stats accumulator - handle_undeclared_ssrc rejects RTX codec as primary Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8c6cc18 to
b83017f
Compare
The test called register_rtx_ssrc() but the actual method on RTCStatsAccumulator is update_inbound_rtx_ssrc(). Fixed the test to use the correct method name and argument order. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Three bugs caused all RTX/FEC repair packets to be silently dropped:
Bug 1 —
find_track_id_by_ssrc: outer SSRC search misses repair SSRCsRTX/FEC SSRCs live in
coding.rtx.ssrc/coding.fec.ssrc, notcoding.ssrc. The outer transceiver search only checkedcoding.ssrc, so any packet with an RTX or FEC SSRC fell through toNoneand was dropped. Fix: extend the.any()predicate to also checkrtx/fecSSRCs. When a match is found via repair SSRC, the packet is routed to the primary stream's receiver immediately without firingOnOpenevents.Bug 2 —
find_track_id_by_rid(rridbranch): always returnedNoneThe
rridheader extension identifies a repair packet for a base stream identified byrid. The code correctly updatedcoding.rtx.ssrc = ssrc, but then fell through toNoneinstead of returning the primary stream'strack_id. Every RTX packet carryingrridwas dropped. Fix: addreturn Some(receiver.track().track_id().clone())after the SSRC update.Bug 3 —
handle_undeclared_ssrc: RTX codec matched as primaryRTX codec entries are included in
get_codec_preferences()(added during SDP negotiation). If an RTX packet arrived first in the single-section non-rid path, the codec lookup would find the RTX codec and incorrectly set it up as the primary track's codec. Fix: exclude RTX mime-type codecs from the lookup so only primary codecs are accepted.Review feedback addressed
find_track_id_by_ssrcto usefind_mapwith a single-pass SSRC classification (primary vs repair), eliminating duplicatereceiver()andget_coding_parameters()calls on the hot RTP receive path.is_repair_mime_typehelper, matching its comment.MIME_TYPE_RTX(video/rtx) check with a generalis_repair_mime_type()helper that covers all RTX variants (video/rtx,audio/rtx) and FEC types (ulpfec,flexfec,flexfec-03) case-insensitively.register_rtx_ssrc()toRTCStatsAccumulatorso late-discovered RTX SSRCs (viarridheader extension) are registered in the reverse lookup map, enablingon_rtx_packet_received_if_rtx()to track retransmission stats.is_repair_mime_type+ 7 integration-style tests covering RTX/FEC SSRC routing, no-OnOpen for repair SSRCs, rrid stats registration, and undeclared-SSRC RTX rejection.{MediaEngine, MIME_TYPE_RTX}-> alphabetical order to passcargo fmt.RTCRtpRtxParametersimport (was causing compile error).Test plan
cargo fmt --check— passescargo check— passescargo clippy— passescargo test -p rtc— all tests pass (173 unit + 76 doc tests)OnOpenis not fired for RTX SSRC sub-streams (unit test)🤖 Generated with Claude Code