Skip to content

Commit abe3c96

Browse files
authored
Remove duplicate mid (#55)
* Created a fix such that duplicated mid's does not occur in the sdp's if mids has already been negotiated. * Use the mid string instead of integer indexes into the vector holding the transcievers. This way the code is simpler to reason about at the cost of a string set instead of an integer set. * cargo fmt
1 parent 939dfe5 commit abe3c96

File tree

2 files changed

+133
-4
lines changed

2 files changed

+133
-4
lines changed

rtc/src/peer_connection/internal.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,12 +272,19 @@ where
272272

273273
// If we are offering also include unmatched local transceivers
274274
let match_bundle_group = if include_unmatched {
275-
for (i, t) in self.rtp_transceivers.iter_mut().enumerate() {
276-
if let Some(sender) = t.sender_mut() {
277-
sender.set_negotiated();
278-
}
275+
let already_matched: HashSet<String> =
276+
media_sections.iter().map(|s| s.mid.clone()).collect();
279277

278+
for (i, t) in self.rtp_transceivers.iter_mut().enumerate() {
280279
if let Some(mid) = t.mid().clone() {
280+
if already_matched.contains(&mid) {
281+
continue;
282+
}
283+
284+
if let Some(sender) = t.sender_mut() {
285+
sender.set_negotiated();
286+
}
287+
281288
media_sections.push(MediaSection {
282289
mid,
283290
transceiver_index: i,
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/// Regression test for duplicate mid bug in generate_matched_sdp().
2+
///
3+
/// When create_offer() is called for a renegotiation (i.e. current_remote_description
4+
/// exists), the function iterates remote media sections to build matched entries, then
5+
/// iterates all local transceivers for unmatched ones. Without the fix, transceivers
6+
/// already matched from the remote description would be added again, producing duplicate
7+
/// m-sections with the same mid in the generated SDP.
8+
use std::collections::HashSet;
9+
10+
use rtc::media_stream::MediaStreamTrack;
11+
use rtc::peer_connection::RTCPeerConnectionBuilder;
12+
use rtc::peer_connection::configuration::media_engine::{MIME_TYPE_VP8, MediaEngine};
13+
use rtc::peer_connection::sdp::RTCSessionDescription;
14+
use rtc::rtp_transceiver::rtp_sender::{
15+
RTCRtpCodec, RTCRtpCodingParameters, RTCRtpEncodingParameters, RtpCodecKind,
16+
};
17+
use sansio::Protocol;
18+
19+
fn new_video_track(stream_id: &str, track_id: &str, ssrc: u32) -> MediaStreamTrack {
20+
MediaStreamTrack::new(
21+
stream_id.to_owned(),
22+
track_id.to_owned(),
23+
format!("track-{}", track_id),
24+
RtpCodecKind::Video,
25+
vec![RTCRtpEncodingParameters {
26+
rtp_coding_parameters: RTCRtpCodingParameters {
27+
ssrc: Some(ssrc),
28+
..Default::default()
29+
},
30+
codec: RTCRtpCodec {
31+
mime_type: MIME_TYPE_VP8.to_owned(),
32+
clock_rate: 90000,
33+
channels: 0,
34+
sdp_fmtp_line: String::new(),
35+
rtcp_feedback: vec![],
36+
},
37+
..Default::default()
38+
}],
39+
)
40+
}
41+
42+
/// Collect all mid values from a parsed SDP's media descriptions.
43+
fn collect_mids(sdp: &RTCSessionDescription) -> Vec<String> {
44+
let parsed = sdp.unmarshal().expect("failed to parse SDP");
45+
parsed
46+
.media_descriptions
47+
.iter()
48+
.filter_map(|m| m.attribute("mid").and_then(|v| v.map(|s| s.to_owned())))
49+
.collect()
50+
}
51+
52+
/// After a full offer/answer exchange, a renegotiation offer must not contain
53+
/// duplicate mids for transceivers that were already matched from the remote
54+
/// description.
55+
#[test]
56+
fn test_renegotiation_no_duplicate_mids() -> Result<(), Box<dyn std::error::Error>> {
57+
let mut me = MediaEngine::default();
58+
me.register_default_codecs()?;
59+
let me2 = me.clone();
60+
61+
// --- Offerer ---
62+
let mut offerer = RTCPeerConnectionBuilder::new()
63+
.with_media_engine(me)
64+
.build()?;
65+
66+
// Add an initial video track.
67+
let track1 = new_video_track("stream-1", "video-1", 11111);
68+
offerer.add_track(track1)?;
69+
70+
// --- Answerer ---
71+
let mut answerer = RTCPeerConnectionBuilder::new()
72+
.with_media_engine(me2)
73+
.build()?;
74+
75+
// ---- First offer/answer exchange ----
76+
let offer1 = offerer.create_offer(None)?;
77+
offerer.set_local_description(offer1.clone())?;
78+
79+
answerer.set_remote_description(offer1)?;
80+
let answer1 = answerer.create_answer(None)?;
81+
answerer.set_local_description(answer1.clone())?;
82+
83+
offerer.set_remote_description(answer1)?;
84+
85+
// Sanity: first offer has unique mids.
86+
let offer1_for_check = offerer.create_offer(None)?;
87+
let mids = collect_mids(&offer1_for_check);
88+
let unique: HashSet<_> = mids.iter().collect();
89+
assert_eq!(
90+
mids.len(),
91+
unique.len(),
92+
"first renegotiation offer already has duplicate mids: {:?}",
93+
mids,
94+
);
95+
96+
// ---- Add a second video track and renegotiate ----
97+
let track2 = new_video_track("stream-2", "video-2", 22222);
98+
offerer.add_track(track2)?;
99+
100+
let offer2 = offerer.create_offer(None)?;
101+
let mids = collect_mids(&offer2);
102+
let unique: HashSet<_> = mids.iter().collect();
103+
assert_eq!(
104+
mids.len(),
105+
unique.len(),
106+
"renegotiation offer has duplicate mids: {:?}",
107+
mids,
108+
);
109+
110+
// Verify we have the expected number of media sections (1 video + 1 new video = 2).
111+
assert_eq!(
112+
mids.len(),
113+
2,
114+
"expected 2 media sections, got {}: {:?}",
115+
mids.len(),
116+
mids,
117+
);
118+
119+
offerer.close()?;
120+
answerer.close()?;
121+
Ok(())
122+
}

0 commit comments

Comments
 (0)