From 71e8f844f1cbcf094e44b0ee1a84866a910ebf0f Mon Sep 17 00:00:00 2001 From: nightness Date: Wed, 1 Apr 2026 07:50:11 -0500 Subject: [PATCH] fix(endpoint): route RTX/FEC repair packets to primary stream receiver 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 --- rtc/src/peer_connection/handler/endpoint.rs | 49 ++++++++++++++++++--- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/rtc/src/peer_connection/handler/endpoint.rs b/rtc/src/peer_connection/handler/endpoint.rs index 33fff14..97355e1 100644 --- a/rtc/src/peer_connection/handler/endpoint.rs +++ b/rtc/src/peer_connection/handler/endpoint.rs @@ -8,7 +8,7 @@ use crate::peer_connection::message::internal::{ }; use crate::media_stream::track::MediaStreamTrackId; -use crate::peer_connection::configuration::media_engine::MediaEngine; +use crate::peer_connection::configuration::media_engine::{MediaEngine, MIME_TYPE_RTX}; use crate::peer_connection::event::track_event::{RTCTrackEvent, RTCTrackEventInit}; use crate::rtp_transceiver::rtp_receiver::internal::RTCRtpReceiverInternal; use crate::rtp_transceiver::rtp_sender::{RTCRtpCodingParameters, RTCRtpHeaderExtensionCapability}; @@ -310,12 +310,31 @@ where if let Some(receiver) = transceiver.receiver() { receiver.get_coding_parameters().iter().any(|coding| { coding.ssrc.is_some_and(|coding_ssrc| coding_ssrc == ssrc) + // Also match RTX/FEC repair SSRCs so repair packets are routed to + // the primary stream's receiver rather than silently dropped. + || coding.rtx.as_ref().is_some_and(|r| r.ssrc == ssrc) + || coding.fec.as_ref().is_some_and(|f| f.ssrc == ssrc) }) } else { false } }) { + // 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. + let is_repair_ssrc = transceiver.receiver().as_ref().is_some_and(|receiver| { + receiver.get_coding_parameters().iter().any(|coding| { + coding.rtx.as_ref().is_some_and(|r| r.ssrc == ssrc) + || coding.fec.as_ref().is_some_and(|f| f.ssrc == ssrc) + }) + }); + if is_repair_ssrc { + return transceiver + .receiver() + .as_ref() + .map(|r| r.track().track_id().clone()); + } + // Get kind and mid before borrowing receiver mutably let kind = transceiver.kind(); let mid = transceiver.mid().clone().unwrap_or_default(); @@ -334,13 +353,14 @@ where receiver.track().track_id().clone(), ); + // 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 { @@ -444,11 +464,22 @@ where && let Some(codec) = receiver .get_codec_preferences() .iter() - .find(|codec| codec.payload_type == rtp_header.payload_type) //TODO: what about RTX/FEC stream? + // Accept both primary and RTX codecs here; the rrid branch handles repair + // packets (it only needs the codec lookup to succeed to enter the block). + .find(|codec| codec.payload_type == rtp_header.payload_type) .cloned() { if !rrid.is_empty() { - //TODO: Add support of handling repair rtp stream id (rrid) #12 + // 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()); } else { if let Some(coding) = receiver.get_coding_parameter_mut_by_rid(rid.as_str()) { coding.ssrc = Some(ssrc); @@ -534,7 +565,15 @@ where && let Some(codec) = receiver .get_codec_preferences() .iter() - .find(|codec| codec.payload_type == rtp_header.payload_type) //TODO: what about RTX/FEC stream? + // Only match primary codecs here; RTX/FEC repair packets are handled + // via find_track_id_by_ssrc once their SSRC is registered. + .find(|codec| { + codec.payload_type == rtp_header.payload_type + && !codec + .rtp_codec + .mime_type + .eq_ignore_ascii_case(MIME_TYPE_RTX) + }) .cloned() { let receive_codings = vec![RTCRtpCodingParameters {