use rtc::media_stream::MediaStreamTrack;
use rtc::peer_connection::RTCPeerConnection;
use rtc::peer_connection::configuration::media_engine::{
MIME_TYPE_OPUS, MIME_TYPE_VP8, MediaEngine,
};
use rtc::peer_connection::sdp::RTCSessionDescription;
use rtc::rtp_transceiver::rtp_sender::{
RTCRtpCodec, RTCRtpCodingParameters, RTCRtpEncodingParameters, RtpCodecKind,
};
use rtc::sansio::Protocol;
fn media_pc() -> RTCPeerConnection {
let mut me = MediaEngine::default();
me.register_default_codecs().unwrap();
rtc::peer_connection::RTCPeerConnectionBuilder::new()
.with_media_engine(me)
.build()
.unwrap()
}
fn track(kind: RtpCodecKind, id: &str, ssrc: u32) -> MediaStreamTrack {
let (mime_type, clock_rate, channels) = match kind {
RtpCodecKind::Audio => (MIME_TYPE_OPUS.to_owned(), 48000, 2),
_ => (MIME_TYPE_VP8.to_owned(), 90000, 0),
};
MediaStreamTrack::new(
format!("stream-{id}"),
format!("track-{id}"),
format!("label-{id}"),
kind,
vec![RTCRtpEncodingParameters {
rtp_coding_parameters: RTCRtpCodingParameters {
ssrc: Some(ssrc),
..Default::default()
},
codec: RTCRtpCodec {
mime_type,
clock_rate,
channels,
sdp_fmtp_line: String::new(),
rtcp_feedback: vec![],
},
..Default::default()
}],
)
}
fn rollback() -> RTCSessionDescription {
RTCSessionDescription::rollback(None).unwrap()
}
fn drain_events(pc: &mut RTCPeerConnection) {
while pc.poll_event().is_some() {}
}
fn count_media(sdp: &RTCSessionDescription, media: &str) -> usize {
sdp.sdp.matches(&format!("m={media}")).count()
}
#[test]
fn polite_peer_glare_recovery_with_local_offer_rollback() {
let mut polite = media_pc();
polite
.add_track(track(RtpCodecKind::Video, "polite", 11111))
.unwrap();
let polite_offer = polite.create_offer(None).unwrap();
polite.set_local_description(polite_offer).unwrap();
drain_events(&mut polite);
let mut impolite = media_pc();
impolite
.add_track(track(RtpCodecKind::Audio, "impolite", 22222))
.unwrap();
let impolite_offer = impolite.create_offer(None).unwrap();
impolite
.set_local_description(impolite_offer.clone())
.unwrap();
polite.set_local_description(rollback()).unwrap();
drain_events(&mut polite);
assert!(
polite.pending_local_description().is_none(),
"rollback must clear the pending local description"
);
polite.set_remote_description(impolite_offer).unwrap();
let answer = polite.create_answer(None).unwrap();
assert_eq!(
count_media(&answer, "audio"),
1,
"answer must contain exactly the remote offer's single audio section"
);
assert_eq!(
count_media(&answer, "video"),
0,
"the rolled-back video track must not appear in the answer"
);
polite.set_local_description(answer).unwrap();
drain_events(&mut polite);
let followup = polite.create_offer(None).unwrap();
assert_eq!(
count_media(&followup, "audio"),
1,
"follow-up offer must retain the negotiated audio section: {}",
followup.sdp
);
assert_eq!(
count_media(&followup, "video"),
1,
"follow-up offer must renegotiate the rolled-back local video track: {}",
followup.sdp
);
}
#[test]
fn glare_recovery_with_remote_offer_rollback() {
let mut pc = media_pc();
pc.add_track(track(RtpCodecKind::Video, "local", 33333))
.unwrap();
let mut remote = media_pc();
remote
.add_track(track(RtpCodecKind::Audio, "remote", 44444))
.unwrap();
let remote_offer = remote.create_offer(None).unwrap();
pc.set_remote_description(remote_offer).unwrap();
drain_events(&mut pc);
pc.set_remote_description(rollback()).unwrap();
drain_events(&mut pc);
assert!(
pc.pending_remote_description().is_none(),
"rollback must clear the pending remote description"
);
let followup = pc.create_offer(None).unwrap();
assert_eq!(
count_media(&followup, "video"),
1,
"offer after remote-offer rollback must include the app's own added video track: {}",
followup.sdp
);
assert_eq!(
count_media(&followup, "audio"),
0,
"the transceiver created by the rolled-back remote offer must be removed: {}",
followup.sdp
);
}