1use crate::call::active_call::ActiveCallStateRef;
2use crate::callrecord::CallRecordHangupReason;
3use crate::event::EventSender;
4use crate::media::TrackId;
5use crate::media::stream::MediaStream;
6use crate::useragent::invitation::PendingDialog;
7use anyhow::Result;
8use chrono::Utc;
9use rsipstack::dialog::DialogId;
10use rsipstack::dialog::dialog::{
11 Dialog, DialogState, DialogStateReceiver, DialogStateSender, TerminatedReason,
12};
13use rsipstack::dialog::dialog_layer::DialogLayer;
14use rsipstack::dialog::invitation::InviteOption;
15use rsipstack::rsip_ext::RsipResponseExt;
16use std::collections::HashMap;
17use std::sync::Arc;
18use tokio_util::sync::CancellationToken;
19use tracing::{info, warn};
20
21pub struct DialogStateReceiverGuard {
22 pub(super) dialog_layer: Arc<DialogLayer>,
23 pub(super) receiver: DialogStateReceiver,
24 pub(super) dialog_id: Option<DialogId>,
25 pub(super) hangup_headers: Option<Vec<rsip::Header>>,
26}
27
28impl DialogStateReceiverGuard {
29 pub fn new(
30 dialog_layer: Arc<DialogLayer>,
31 receiver: DialogStateReceiver,
32 hangup_headers: Option<Vec<rsip::Header>>,
33 ) -> Self {
34 Self {
35 dialog_layer,
36 receiver,
37 dialog_id: None,
38 hangup_headers,
39 }
40 }
41 pub async fn recv(&mut self) -> Option<DialogState> {
42 let state = self.receiver.recv().await;
43 if let Some(ref s) = state {
44 self.dialog_id = Some(s.id().clone());
45 }
46 state
47 }
48
49 fn take_dialog(&mut self) -> Option<Dialog> {
50 let id = match self.dialog_id.take() {
51 Some(id) => id,
52 None => return None,
53 };
54
55 match self.dialog_layer.get_dialog(&id) {
56 Some(dialog) => {
57 info!(%id, "dialog removed on drop");
58 self.dialog_layer.remove_dialog(&id);
59 return Some(dialog);
60 }
61 _ => {}
62 }
63 None
64 }
65
66 pub async fn drop_async(&mut self) {
67 if let Some(dialog) = self.take_dialog() {
68 if let Err(e) = dialog.hangup_with_headers(self.hangup_headers.take()).await {
69 warn!(id=%dialog.id(), "error hanging up dialog on drop: {}", e);
70 }
71 }
72 }
73}
74
75impl Drop for DialogStateReceiverGuard {
76 fn drop(&mut self) {
77 if let Some(dialog) = self.take_dialog() {
78 crate::spawn(async move {
79 if let Err(e) = dialog.hangup().await {
80 warn!(id=%dialog.id(), "error hanging up dialog on drop: {}", e);
81 }
82 });
83 }
84 }
85}
86
87pub(super) struct InviteDialogStates {
88 pub is_client: bool,
89 pub session_id: String,
90 pub track_id: TrackId,
91 pub cancel_token: CancellationToken,
92 pub event_sender: EventSender,
93 pub call_state: ActiveCallStateRef,
94 pub media_stream: Arc<MediaStream>,
95 pub terminated_reason: Option<TerminatedReason>,
96 pub has_early_media: bool,
97}
98
99impl InviteDialogStates {
100 pub(super) fn on_terminated(&mut self) {
101 let mut call_state_ref = match self.call_state.try_write() {
102 Ok(cs) => cs,
103 Err(_) => {
104 return;
105 }
106 };
107 let reason = &self.terminated_reason;
108 call_state_ref.last_status_code = match reason {
109 Some(TerminatedReason::UacCancel) => 487,
110 Some(TerminatedReason::UacBye) => 200,
111 Some(TerminatedReason::UacBusy) => 486,
112 Some(TerminatedReason::UasBye) => 200,
113 Some(TerminatedReason::UasBusy) => 486,
114 Some(TerminatedReason::UasDecline) => 603,
115 Some(TerminatedReason::UacOther(code)) => code.code(),
116 Some(TerminatedReason::UasOther(code)) => code.code(),
117 _ => 500, };
119
120 if call_state_ref.hangup_reason.is_none() {
121 call_state_ref.hangup_reason.replace(match reason {
122 Some(TerminatedReason::UacCancel) => CallRecordHangupReason::Canceled,
123 Some(TerminatedReason::UacBye) | Some(TerminatedReason::UacBusy) => {
124 CallRecordHangupReason::ByCaller
125 }
126 Some(TerminatedReason::UasBye) | Some(TerminatedReason::UasBusy) => {
127 CallRecordHangupReason::ByCallee
128 }
129 Some(TerminatedReason::UasDecline) => CallRecordHangupReason::ByCallee,
130 Some(TerminatedReason::UacOther(_)) => CallRecordHangupReason::ByCaller,
131 Some(TerminatedReason::UasOther(_)) => CallRecordHangupReason::ByCallee,
132 _ => CallRecordHangupReason::BySystem,
133 });
134 };
135 let initiator = match reason {
136 Some(TerminatedReason::UacCancel) => "caller".to_string(),
137 Some(TerminatedReason::UacBye) | Some(TerminatedReason::UacBusy) => {
138 "caller".to_string()
139 }
140 Some(TerminatedReason::UasBye)
141 | Some(TerminatedReason::UasBusy)
142 | Some(TerminatedReason::UasDecline) => "callee".to_string(),
143 _ => "system".to_string(),
144 };
145 self.event_sender
146 .send(crate::event::SessionEvent::TrackEnd {
147 track_id: self.track_id.clone(),
148 timestamp: crate::media::get_timestamp(),
149 duration: call_state_ref
150 .answer_time
151 .map(|t| (Utc::now() - t).num_milliseconds())
152 .unwrap_or_default() as u64,
153 ssrc: call_state_ref.ssrc,
154 play_id: None,
155 })
156 .ok();
157 let hangup_event =
158 call_state_ref.build_hangup_event(self.track_id.clone(), Some(initiator));
159 self.event_sender.send(hangup_event).ok();
160 }
161}
162
163impl Drop for InviteDialogStates {
164 fn drop(&mut self) {
165 self.on_terminated();
166 self.cancel_token.cancel();
167 }
168}
169
170impl DialogStateReceiverGuard {
171 pub(self) async fn dialog_event_loop(&mut self, states: &mut InviteDialogStates) -> Result<()> {
172 while let Some(event) = self.recv().await {
173 match event {
174 DialogState::Calling(dialog_id) => {
175 info!(session_id=states.session_id, %dialog_id, "dialog calling");
176 states.call_state.write().await.session_id = dialog_id.to_string();
177 }
178 DialogState::Trying(_) => {}
179 DialogState::Early(dialog_id, resp) => {
180 let code = resp.status_code.code();
181 let body = resp.body();
182 let answer = String::from_utf8_lossy(body);
183 let has_sdp = !answer.is_empty();
184 info!(session_id=states.session_id, %dialog_id, has_sdp=%has_sdp, "dialog early ({}): \n{}", code, answer);
185
186 {
187 let mut cs = states.call_state.write().await;
188 if cs.ring_time.is_none() {
189 cs.ring_time.replace(Utc::now());
190 }
191 cs.last_status_code = code;
192 }
193
194 if !states.is_client {
195 continue;
196 }
197
198 let refer = states.call_state.read().await.is_refer;
199
200 states
201 .event_sender
202 .send(crate::event::SessionEvent::Ringing {
203 track_id: states.track_id.clone(),
204 timestamp: crate::media::get_timestamp(),
205 early_media: has_sdp,
206 refer: Some(refer),
207 })?;
208
209 if has_sdp {
210 states.has_early_media = true;
211 {
212 let mut cs = states.call_state.write().await;
213 if cs.answer.is_none() {
214 cs.answer = Some(answer.to_string());
215 }
216 }
217 states
218 .media_stream
219 .update_remote_description(&states.track_id, &answer.to_string())
220 .await?;
221 }
222 }
223 DialogState::Confirmed(dialog_id, msg) => {
224 info!(session_id=states.session_id, %dialog_id, has_early_media=%states.has_early_media, "dialog confirmed");
225 {
226 let mut cs = states.call_state.write().await;
227 cs.session_id = dialog_id.to_string();
228 cs.answer_time.replace(Utc::now());
229 cs.last_status_code = 200;
230 }
231 if states.is_client {
232 let answer = String::from_utf8_lossy(msg.body());
233 let answer = answer.trim();
234 if !answer.is_empty() {
235 if states.has_early_media {
236 info!(
237 session_id = states.session_id,
238 "updating remote description with final answer after early media (force=true)"
239 );
240 if let Err(e) = states
243 .media_stream
244 .update_remote_description_force(
245 &states.track_id,
246 &answer.to_string(),
247 )
248 .await
249 {
250 tracing::warn!(
251 session_id = states.session_id,
252 "failed to force update remote description on confirmed: {}",
253 e
254 );
255 }
256 } else {
257 if let Err(e) = states
258 .media_stream
259 .update_remote_description(
260 &states.track_id,
261 &answer.to_string(),
262 )
263 .await
264 {
265 tracing::warn!(
266 session_id = states.session_id,
267 "failed to update remote description on confirmed: {}",
268 e
269 );
270 }
271 }
272 }
273 }
274 }
275 DialogState::Info(dialog_id, req, tx_handle) => {
276 let body_str = String::from_utf8_lossy(req.body());
277 info!(session_id=states.session_id, %dialog_id, body=%body_str, "dialog info received");
278 if body_str.starts_with("Signal=") {
279 let digit = body_str.trim_start_matches("Signal=").chars().next();
280 if let Some(digit) = digit {
281 states.event_sender.send(crate::event::SessionEvent::Dtmf {
282 track_id: states.track_id.clone(),
283 timestamp: crate::media::get_timestamp(),
284 digit: digit.to_string(),
285 })?;
286 }
287 }
288 tx_handle.reply(rsip::StatusCode::OK).await.ok();
289 }
290 DialogState::Updated(dialog_id, _req, tx_handle) => {
291 info!(session_id = states.session_id, %dialog_id, "dialog update received");
292 let mut answer_sdp = None;
293 if let Some(sdp_body) = _req.body().get(..) {
294 let sdp_str = String::from_utf8_lossy(sdp_body);
295 if !sdp_str.is_empty()
296 && (_req.method == rsip::Method::Invite
297 || _req.method == rsip::Method::Update)
298 {
299 info!(session_id=states.session_id, %dialog_id, method=%_req.method, "handling re-invite/update offer");
300
301 let is_on_hold =
303 crate::media::negotiate::detect_hold_state_from_sdp(&sdp_str);
304 info!(session_id=states.session_id, %dialog_id, is_on_hold=%is_on_hold, "detected hold state from re-invite SDP");
305
306 if is_on_hold {
308 states
309 .media_stream
310 .hold_track(Some(states.track_id.clone()))
311 .await;
312 } else {
313 states
314 .media_stream
315 .resume_track(Some(states.track_id.clone()))
316 .await;
317 }
318
319 states
321 .event_sender
322 .send(crate::event::SessionEvent::Hold {
323 track_id: states.track_id.clone(),
324 timestamp: crate::media::get_timestamp(),
325 on_hold: is_on_hold,
326 })
327 .ok();
328
329 match states
330 .media_stream
331 .handshake(&states.track_id, sdp_str.to_string(), None)
332 .await
333 {
334 Ok(sdp) => answer_sdp = Some(sdp),
335 Err(e) => {
336 warn!(
337 session_id = states.session_id,
338 "failed to handle re-invite: {}", e
339 );
340 }
341 }
342 } else {
343 info!(session_id=states.session_id, %dialog_id, "updating remote description:\n{}", sdp_str);
344
345 let is_on_hold =
347 crate::media::negotiate::detect_hold_state_from_sdp(&sdp_str);
348 if is_on_hold {
349 states
350 .media_stream
351 .hold_track(Some(states.track_id.clone()))
352 .await;
353 states
354 .event_sender
355 .send(crate::event::SessionEvent::Hold {
356 track_id: states.track_id.clone(),
357 timestamp: crate::media::get_timestamp(),
358 on_hold: true,
359 })
360 .ok();
361 } else {
362 states
363 .media_stream
364 .resume_track(Some(states.track_id.clone()))
365 .await;
366 states
367 .event_sender
368 .send(crate::event::SessionEvent::Hold {
369 track_id: states.track_id.clone(),
370 timestamp: crate::media::get_timestamp(),
371 on_hold: false,
372 })
373 .ok();
374 }
375
376 states
377 .media_stream
378 .update_remote_description(&states.track_id, &sdp_str.to_string())
379 .await?;
380 }
381 }
382
383 if let Some(sdp) = answer_sdp {
384 tx_handle
385 .respond(
386 rsip::StatusCode::OK,
387 Some(vec![rsip::Header::ContentType(
388 "application/sdp".to_string().into(),
389 )]),
390 Some(sdp.into()),
391 )
392 .await
393 .ok();
394 } else {
395 tx_handle.reply(rsip::StatusCode::OK).await.ok();
396 }
397 }
398 DialogState::Options(dialog_id, _req, tx_handle) => {
399 info!(session_id = states.session_id, %dialog_id, "dialog options received");
400 tx_handle.reply(rsip::StatusCode::OK).await.ok();
401 }
402 DialogState::Terminated(dialog_id, reason) => {
403 info!(
404 session_id = states.session_id,
405 ?dialog_id,
406 ?reason,
407 "dialog terminated"
408 );
409 states.terminated_reason = Some(reason.clone());
410 return Ok(());
411 }
412 other_state => {
413 info!(
414 session_id = states.session_id,
415 %other_state,
416 "dialog received other state"
417 );
418 }
419 }
420 }
421 Ok(())
422 }
423
424 pub(super) async fn process_dialog(&mut self, mut states: InviteDialogStates) {
425 let token = states.cancel_token.clone();
426 tokio::select! {
427 _ = token.cancelled() => {
428 states.terminated_reason = Some(TerminatedReason::UacCancel);
429 }
430 _ = self.dialog_event_loop(&mut states) => {}
431 };
432
433 {
435 let state = states.call_state.read().await;
436 if let Some(extras) = &state.extras {
437 if let Some(h_val) = extras.get("_hangup_headers") {
438 if let Ok(headers_map) =
439 serde_json::from_value::<HashMap<String, String>>(h_val.clone())
440 {
441 let mut headers = Vec::new();
442 for (k, v) in headers_map {
443 headers.push(rsip::Header::Other(k.into(), v.into()));
444 }
445 if !headers.is_empty() {
446 if let Some(existing) = &mut self.hangup_headers {
447 existing.extend(headers);
448 } else {
449 self.hangup_headers = Some(headers);
450 }
451 }
452 }
453 }
454 }
455 }
456
457 self.drop_async().await;
458 }
459}
460
461#[derive(Clone)]
462pub struct Invitation {
463 pub dialog_layer: Arc<DialogLayer>,
464 pub pending_dialogs: Arc<std::sync::Mutex<HashMap<DialogId, PendingDialog>>>,
465}
466
467impl Invitation {
468 pub fn new(dialog_layer: Arc<DialogLayer>) -> Self {
469 Self {
470 dialog_layer,
471 pending_dialogs: Arc::new(std::sync::Mutex::new(HashMap::new())),
472 }
473 }
474
475 pub fn add_pending(&self, dialog_id: DialogId, pending: PendingDialog) {
476 self.pending_dialogs
477 .lock()
478 .map(|mut ps| ps.insert(dialog_id, pending))
479 .ok();
480 }
481
482 pub fn get_pending_call(&self, dialog_id: &DialogId) -> Option<PendingDialog> {
483 self.pending_dialogs
484 .lock()
485 .ok()
486 .and_then(|mut ps| ps.remove(dialog_id))
487 }
488
489 pub fn has_pending_call(&self, dialog_id: &DialogId) -> bool {
490 self.pending_dialogs
491 .lock()
492 .ok()
493 .map(|ps| ps.contains_key(dialog_id))
494 .unwrap_or(false)
495 }
496
497 pub fn find_dialog_id_by_session_id(&self, session_id: &str) -> Option<DialogId> {
498 self.pending_dialogs.lock().ok().and_then(|ps| {
499 ps.iter()
500 .find(|(id, _)| id.to_string() == session_id)
501 .map(|(id, _)| id.clone())
502 })
503 }
504
505 pub async fn hangup(
506 &self,
507 dialog_id: DialogId,
508 code: Option<rsip::StatusCode>,
509 reason: Option<String>,
510 ) -> Result<()> {
511 if let Some(call) = self.get_pending_call(&dialog_id) {
512 call.dialog.reject(code, reason).ok();
513 call.token.cancel();
514 }
515 match self.dialog_layer.get_dialog(&dialog_id) {
516 Some(dialog) => {
517 self.dialog_layer.remove_dialog(&dialog_id);
518 dialog.hangup().await.ok();
519 }
520 None => {}
521 }
522 Ok(())
523 }
524
525 pub async fn reject(&self, dialog_id: DialogId) -> Result<()> {
526 if let Some(call) = self.get_pending_call(&dialog_id) {
527 call.dialog.reject(None, None).ok();
528 call.token.cancel();
529 }
530 match self.dialog_layer.get_dialog(&dialog_id) {
531 Some(dialog) => {
532 self.dialog_layer.remove_dialog(&dialog_id);
533 dialog.hangup().await.ok();
534 }
535 None => {}
536 }
537 Ok(())
538 }
539
540 pub async fn invite(
541 &self,
542 invite_option: InviteOption,
543 state_sender: DialogStateSender,
544 ) -> Result<(DialogId, Option<Vec<u8>>), rsipstack::Error> {
545 let (dialog, resp) = self
546 .dialog_layer
547 .do_invite(invite_option, state_sender)
548 .await?;
549
550 let offer = match resp {
551 Some(resp) => match resp.status_code.kind() {
552 rsip::StatusCodeKind::Successful => {
553 let offer = resp.body.clone();
554 Some(offer)
555 }
556 _ => {
557 let reason = resp
558 .reason_phrase()
559 .unwrap_or(&resp.status_code.to_string())
560 .to_string();
561 return Err(rsipstack::Error::DialogError(
562 reason,
563 dialog.id(),
564 resp.status_code,
565 ));
566 }
567 },
568 None => {
569 return Err(rsipstack::Error::DialogError(
570 "no response received".to_string(),
571 dialog.id(),
572 rsip::StatusCode::NotAcceptableHere,
573 ));
574 }
575 };
576 Ok((dialog.id(), offer))
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583 use crate::call::active_call::ActiveCallState;
584 use crate::media::stream::MediaStreamBuilder;
585 use std::sync::Arc;
586 use tokio::sync::RwLock;
587 use tokio_util::sync::CancellationToken;
588
589 const EARLY_MEDIA_SDP: &str = "v=0\r\n\
591 o=- 1000 1 IN IP4 192.168.1.100\r\n\
592 s=SIP Call\r\n\
593 t=0 0\r\n\
594 m=audio 10000 RTP/AVP 0\r\n\
595 c=IN IP4 192.168.1.100\r\n\
596 a=rtpmap:0 PCMU/8000\r\n\
597 a=sendrecv\r\n";
598
599 fn make_response_with_body(body: Vec<u8>) -> rsip::Response {
600 let mut resp = rsip::Response::default();
601 resp.body = body;
602 resp
603 }
604
605 #[tokio::test]
609 async fn test_early_sdp_stored_in_call_state() {
610 let (event_tx, _event_rx) = tokio::sync::broadcast::channel(16);
611 let media_stream = Arc::new(
612 MediaStreamBuilder::new(event_tx.clone())
613 .with_id("test-stream".to_string())
614 .build(),
615 );
616 let call_state: ActiveCallStateRef = Arc::new(RwLock::new(ActiveCallState::default()));
617 let cancel_token = CancellationToken::new();
618
619 let mut states = InviteDialogStates {
620 is_client: true,
621 session_id: "test-session".to_string(),
622 track_id: "test-track".to_string(),
623 cancel_token: cancel_token.clone(),
624 event_sender: event_tx.clone(),
625 call_state: call_state.clone(),
626 media_stream: media_stream.clone(),
627 terminated_reason: None,
628 has_early_media: false,
629 };
630
631 let early_resp = make_response_with_body(EARLY_MEDIA_SDP.as_bytes().to_vec());
633
634 let body = early_resp.body();
636 let answer = String::from_utf8_lossy(body);
637 let has_sdp = !answer.is_empty();
638 if states.is_client && has_sdp {
639 states.has_early_media = true;
640 {
641 let mut cs = states.call_state.write().await;
642 if cs.answer.is_none() {
643 cs.answer = Some(answer.to_string());
644 }
645 }
646 }
648
649 {
651 let cs = call_state.read().await;
652 assert!(
653 cs.answer.is_some(),
654 "call_state.answer should be set after 183 with SDP"
655 );
656 assert_eq!(
657 cs.answer.as_deref().unwrap(),
658 EARLY_MEDIA_SDP,
659 "call_state.answer should contain the early SDP"
660 );
661 }
662 assert!(states.has_early_media, "has_early_media should be true");
663 }
664
665 #[tokio::test]
672 async fn test_confirmed_empty_body_keeps_early_sdp() {
673 let (event_tx, _event_rx) = tokio::sync::broadcast::channel(16);
674 let media_stream = Arc::new(
675 MediaStreamBuilder::new(event_tx.clone())
676 .with_id("test-stream-2".to_string())
677 .build(),
678 );
679 let call_state: ActiveCallStateRef = Arc::new(RwLock::new(ActiveCallState::default()));
680 let cancel_token = CancellationToken::new();
681
682 let mut states = InviteDialogStates {
683 is_client: true,
684 session_id: "test-session-2".to_string(),
685 track_id: "test-track-2".to_string(),
686 cancel_token: cancel_token.clone(),
687 event_sender: event_tx.clone(),
688 call_state: call_state.clone(),
689 media_stream: media_stream.clone(),
690 terminated_reason: None,
691 has_early_media: false,
692 };
693
694 {
696 let answer_str = EARLY_MEDIA_SDP.to_string();
697 states.has_early_media = true;
698 let mut cs = states.call_state.write().await;
699 if cs.answer.is_none() {
700 cs.answer = Some(answer_str);
701 }
702 }
703
704 let confirmed_resp = make_response_with_body(vec![]); {
707 let mut cs = states.call_state.write().await;
708 cs.answer_time.replace(chrono::Utc::now());
709 cs.last_status_code = 200;
710 }
711 let body = confirmed_resp.body();
714 let answer = String::from_utf8_lossy(body);
715 let answer_trimmed = answer.trim();
716 if states.is_client && !answer_trimmed.is_empty() {
718 panic!("Confirmed handler should not update SDP for empty body");
721 }
722
723 {
725 let cs = call_state.read().await;
726 assert!(
727 cs.answer.is_some(),
728 "call_state.answer must not be None after 200 OK with empty body"
729 );
730 let stored_answer = cs.answer.as_deref().unwrap();
731 assert!(
732 !stored_answer.is_empty(),
733 "call_state.answer must not be empty after 200 OK with empty body"
734 );
735 assert_eq!(
736 stored_answer, EARLY_MEDIA_SDP,
737 "call_state.answer should still be the early SDP after 200 OK with empty body"
738 );
739 }
740 }
741
742 #[tokio::test]
749 async fn test_answer_fallback_to_early_sdp_when_200ok_empty() {
750 let call_state: ActiveCallStateRef = Arc::new(RwLock::new(ActiveCallState::default()));
752
753 {
755 let mut cs = call_state.write().await;
756 cs.answer = Some(EARLY_MEDIA_SDP.to_string());
757 }
758
759 let raw_answer: Option<Vec<u8>> = Some(vec![]); let resolved_answer = match raw_answer {
764 Some(bytes) => {
765 let s = String::from_utf8_lossy(&bytes).to_string();
766 if s.trim().is_empty() {
767 let cs = call_state.read().await;
769 match cs.answer.clone() {
770 Some(early_sdp) if !early_sdp.is_empty() => {
771 (early_sdp, true )
772 }
773 _ => (s, false),
774 }
775 } else {
776 (s, false)
777 }
778 }
779 None => {
780 let cs = call_state.read().await;
781 match cs.answer.clone() {
782 Some(early_sdp) if !early_sdp.is_empty() => (early_sdp, true),
783 _ => panic!("Expected early SDP fallback"),
784 }
785 }
786 };
787
788 let (answer, already_applied) = resolved_answer;
789
790 assert!(
793 !answer.is_empty(),
794 "Resolved answer must not be empty — should contain the early SDP"
795 );
796 assert_eq!(
797 answer, EARLY_MEDIA_SDP,
798 "Resolved answer should be the early SDP from the 183 handler"
799 );
800 assert!(
801 already_applied,
802 "remote_description_already_applied should be true when using early SDP fallback"
803 );
804 }
805
806 #[tokio::test]
810 async fn test_answer_uses_200ok_sdp_when_present() {
811 const FINAL_SDP: &str = "v=0\r\n\
812 o=- 2000 2 IN IP4 10.0.0.1\r\n\
813 s=SIP Call\r\n\
814 t=0 0\r\n\
815 m=audio 20000 RTP/AVP 0\r\n\
816 c=IN IP4 10.0.0.1\r\n\
817 a=rtpmap:0 PCMU/8000\r\n\
818 a=sendrecv\r\n";
819
820 let call_state: ActiveCallStateRef = Arc::new(RwLock::new(ActiveCallState::default()));
821
822 {
824 let mut cs = call_state.write().await;
825 cs.answer = Some(EARLY_MEDIA_SDP.to_string());
826 }
827
828 let raw_answer: Option<Vec<u8>> = Some(FINAL_SDP.as_bytes().to_vec());
829
830 let resolved_answer = match raw_answer {
831 Some(bytes) => {
832 let s = String::from_utf8_lossy(&bytes).to_string();
833 if s.trim().is_empty() {
834 let cs = call_state.read().await;
835 match cs.answer.clone() {
836 Some(early_sdp) if !early_sdp.is_empty() => (early_sdp, true),
837 _ => (s, false),
838 }
839 } else {
840 (s, false) }
842 }
843 None => panic!("Unexpected"),
844 };
845
846 let (answer, already_applied) = resolved_answer;
847
848 assert_eq!(
849 answer, FINAL_SDP,
850 "When 200 OK has SDP, it should be used (not the early SDP)"
851 );
852 assert!(
853 !already_applied,
854 "remote_description_already_applied should be false when 200 OK has SDP body"
855 );
856 }
857}