From 905aab68eb6fab17febb0659ddbce582f0362062 Mon Sep 17 00:00:00 2001 From: nightness Date: Wed, 1 Apr 2026 05:08:50 -0500 Subject: [PATCH] =?UTF-8?q?fix(sdp):=20reflect=20rejected=20m-lines=20(por?= =?UTF-8?q?t=3D0)=20in=20answer=20per=20RFC=208829=20=C2=A75.3.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an offer contains a rejected m-line (port=0 / no direction), the answer must reflect it in the same position with port=0 to preserve m-line indexing. Without this, re-offers after a rejected section would mis-map subsequent transceivers. Adds `rejected` / `rejected_kind` fields to `MediaSection` (all existing construction sites use `..Default::default()` so there is no breakage). `populate_sdp` emits a minimal port=0 section for rejected lines before the normal transceiver loop. `generate_matched_sdp` now pushes a rejected `MediaSection` instead of silently skipping a section whose direction is `Unspecified`. Co-Authored-By: Claude Sonnet 4.6 --- rtc/src/peer_connection/internal.rs | 18 +++++++++++--- rtc/src/peer_connection/sdp/mod.rs | 38 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/rtc/src/peer_connection/internal.rs b/rtc/src/peer_connection/internal.rs index 63d3627..3746dcf 100644 --- a/rtc/src/peer_connection/internal.rs +++ b/rtc/src/peer_connection/internal.rs @@ -242,9 +242,21 @@ where let kind = RtpCodecKind::from(media.media_name.media.as_str()); let direction = get_peer_direction(media); - if kind == RtpCodecKind::Unspecified - || direction == RTCRtpTransceiverDirection::Unspecified - { + if kind == RtpCodecKind::Unspecified { + continue; + } + + if direction == RTCRtpTransceiverDirection::Unspecified { + // Rejected m-line in the offer (port=0, no direction attribute). + // RFC 8829 §5.3.1: the answer MUST reflect it as rejected to + // preserve m-line indexing across both sides. + media_sections.push(MediaSection { + mid: mid_value.to_owned(), + rejected: true, + rejected_kind: media.media_name.media.clone(), + transceiver_index: usize::MAX, + ..Default::default() + }); continue; } diff --git a/rtc/src/peer_connection/sdp/mod.rs b/rtc/src/peer_connection/sdp/mod.rs index 7ee23f1..0a68f5d 100644 --- a/rtc/src/peer_connection/sdp/mod.rs +++ b/rtc/src/peer_connection/sdp/mod.rs @@ -970,6 +970,11 @@ pub(crate) struct MediaSection { pub(crate) mid: String, pub(crate) transceiver_index: usize, pub(crate) data: bool, + /// RFC 8829 §5.3.1: this m-line should be reflected as rejected (port=0) in the answer. + /// Used when the remote offer contains a rejected m-line (no direction attribute / port=0). + pub(crate) rejected: bool, + /// Media kind string ("video", "audio") — only meaningful when `rejected` is true. + pub(crate) rejected_kind: String, pub(crate) match_extensions: HashMap, pub(crate) rid_map: Vec, } @@ -1062,6 +1067,39 @@ where }; for (i, m) in media_sections.iter().enumerate() { + // RFC 8829 §5.3.1: reflect rejected m-lines (port=0) in the answer. + // These must appear in the same position as in the offer to preserve m-line indexing. + if m.rejected { + d = d.with_media(MediaDescription { + media_name: MediaName { + media: m.rejected_kind.clone(), + port: RangedPort { value: 0, range: None }, + protos: vec![ + "UDP".to_owned(), + "TLS".to_owned(), + "RTP".to_owned(), + "SAVPF".to_owned(), + ], + formats: vec!["0".to_owned()], + }, + media_title: None, + connection_information: Some(ConnectionInformation { + network_type: "IN".to_owned(), + address_type: "IP4".to_owned(), + address: Some(Address { + address: "0.0.0.0".to_owned(), + ttl: None, + range: None, + }), + }), + bandwidth: vec![], + encryption_key: None, + attributes: vec![], + } + .with_value_attribute(ATTR_KEY_MID.to_owned(), m.mid.clone())); + continue; // rejected m-lines are not added to BUNDLE + } + if m.data && m.transceiver_index != usize::MAX { return Err(Error::ErrSDPMediaSectionMediaDataChanInvalid); } else if !m.data && m.transceiver_index >= transceivers.len() {