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