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::Refer(dialog_id, req, tx_handle) => {
402 let refer_to = req.headers.iter().find_map(|h| {
403 if let rsipstack::rsip::Header::ReferTo(h) = h {
404 return Some(h.value().to_string());
405 }
406 None
407 }).unwrap_or_default();
408 let referred_by = req.headers.iter().find_map(|h| {
409 if let rsipstack::rsip::Header::ReferredBy(h) = h {
410 return Some(h.value().to_string());
411 }
412 None
413 });
414 info!(session_id = states.session_id, %dialog_id, %refer_to, "received REFER");
415 tx_handle.reply(rsipstack::rsip::StatusCode::Other(202, "Accepted".into())).await.ok();
416 let is_refer = states.call_state.read().await.is_refer;
417 states.event_sender.send(crate::event::SessionEvent::TransferRequest {
418 track_id: states.track_id.clone(),
419 timestamp: crate::media::get_timestamp(),
420 refer_to,
421 referred_by,
422 refer: Some(is_refer),
423 }).ok();
424 }
425 DialogState::Terminated(dialog_id, reason) => {
426 info!(
427 session_id = states.session_id,
428 ?dialog_id,
429 ?reason,
430 "dialog terminated"
431 );
432 states.terminated_reason = Some(reason.clone());
433 return Ok(());
434 }
435 other_state => {
436 info!(
437 session_id = states.session_id,
438 %other_state,
439 "dialog received other state"
440 );
441 }
442 }
443 }
444 Ok(())
445 }
446
447 pub(super) async fn process_dialog(&mut self, mut states: InviteDialogStates) {
448 let token = states.cancel_token.clone();
449 tokio::select! {
450 _ = token.cancelled() => {
451 states.terminated_reason = Some(TerminatedReason::UacCancel);
452 }
453 _ = self.dialog_event_loop(&mut states) => {}
454 };
455
456 {
458 let state = states.call_state.read().await;
459 if let Some(extras) = &state.extras {
460 if let Some(h_val) = extras.get("_hangup_headers") {
461 if let Ok(headers_map) =
462 serde_json::from_value::<HashMap<String, String>>(h_val.clone())
463 {
464 let mut headers = Vec::new();
465 for (k, v) in headers_map {
466 headers.push(rsipstack::rsip::Header::Other(k.into(), v.into()));
467 }
468 if !headers.is_empty() {
469 if let Some(existing) = &mut self.hangup_headers {
470 existing.extend(headers);
471 } else {
472 self.hangup_headers = Some(headers);
473 }
474 }
475 }
476 }
477 }
478 }
479
480 self.drop_async().await;
481 }
482}
483
484#[derive(Clone)]
485pub struct Invitation {
486 pub dialog_layer: Arc<DialogLayer>,
487 pub pending_dialogs: Arc<std::sync::Mutex<HashMap<DialogId, PendingDialog>>>,
488}
489
490impl Invitation {
491 pub fn new(dialog_layer: Arc<DialogLayer>) -> Self {
492 Self {
493 dialog_layer,
494 pending_dialogs: Arc::new(std::sync::Mutex::new(HashMap::new())),
495 }
496 }
497
498 pub fn add_pending(&self, dialog_id: DialogId, pending: PendingDialog) {
499 self.pending_dialogs
500 .lock()
501 .map(|mut ps| ps.insert(dialog_id, pending))
502 .ok();
503 }
504
505 pub fn get_pending_call(&self, dialog_id: &DialogId) -> Option<PendingDialog> {
506 self.pending_dialogs
507 .lock()
508 .ok()
509 .and_then(|mut ps| ps.remove(dialog_id))
510 }
511
512 pub fn has_pending_call(&self, dialog_id: &DialogId) -> bool {
513 self.pending_dialogs
514 .lock()
515 .ok()
516 .map(|ps| ps.contains_key(dialog_id))
517 .unwrap_or(false)
518 }
519
520 pub fn find_dialog_id_by_session_id(&self, session_id: &str) -> Option<DialogId> {
521 self.pending_dialogs.lock().ok().and_then(|ps| {
522 ps.iter()
523 .find(|(id, _)| id.to_string() == session_id)
524 .map(|(id, _)| id.clone())
525 })
526 }
527
528 pub async fn hangup(
529 &self,
530 dialog_id: DialogId,
531 code: Option<rsipstack::rsip::StatusCode>,
532 reason: Option<String>,
533 ) -> Result<()> {
534 if let Some(call) = self.get_pending_call(&dialog_id) {
535 call.dialog.reject(code, reason).ok();
536 call.token.cancel();
537 }
538 match self.dialog_layer.get_dialog(&dialog_id) {
539 Some(dialog) => {
540 self.dialog_layer.remove_dialog(&dialog_id);
541 dialog.hangup().await.ok();
542 }
543 None => {}
544 }
545 Ok(())
546 }
547
548 pub async fn reject(&self, dialog_id: DialogId) -> Result<()> {
549 if let Some(call) = self.get_pending_call(&dialog_id) {
550 call.dialog.reject(None, None).ok();
551 call.token.cancel();
552 }
553 match self.dialog_layer.get_dialog(&dialog_id) {
554 Some(dialog) => {
555 self.dialog_layer.remove_dialog(&dialog_id);
556 dialog.hangup().await.ok();
557 }
558 None => {}
559 }
560 Ok(())
561 }
562
563 pub async fn invite(
564 &self,
565 invite_option: InviteOption,
566 state_sender: DialogStateSender,
567 ) -> Result<(DialogId, Option<Vec<u8>>), rsipstack::Error> {
568 let (dialog, resp) = self
569 .dialog_layer
570 .do_invite(invite_option, state_sender)
571 .await?;
572
573 let offer = match resp {
574 Some(resp) => match resp.status_code.kind() {
575 rsipstack::rsip::StatusCodeKind::Successful => {
576 let offer = resp.body.clone();
577 Some(offer)
578 }
579 _ => {
580 let reason = resp
581 .reason_phrase()
582 .unwrap_or(&resp.status_code.to_string())
583 .to_string();
584 return Err(rsipstack::Error::DialogError(
585 reason,
586 dialog.id(),
587 resp.status_code,
588 ));
589 }
590 },
591 None => {
592 return Err(rsipstack::Error::DialogError(
593 "no response received".to_string(),
594 dialog.id(),
595 rsipstack::rsip::StatusCode::NotAcceptableHere,
596 ));
597 }
598 };
599 Ok((dialog.id(), offer))
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606 use crate::call::active_call::ActiveCallState;
607 use crate::media::stream::MediaStreamBuilder;
608 use std::sync::Arc;
609 use tokio::sync::RwLock;
610 use tokio_util::sync::CancellationToken;
611
612 const EARLY_MEDIA_SDP: &str = "v=0\r\n\
614 o=- 1000 1 IN IP4 192.168.1.100\r\n\
615 s=SIP Call\r\n\
616 t=0 0\r\n\
617 m=audio 10000 RTP/AVP 0\r\n\
618 c=IN IP4 192.168.1.100\r\n\
619 a=rtpmap:0 PCMU/8000\r\n\
620 a=sendrecv\r\n";
621
622 fn make_response_with_body(body: Vec<u8>) -> rsipstack::rsip::Response {
623 let mut resp = rsipstack::rsip::Response::default();
624 resp.body = body;
625 resp
626 }
627
628 #[tokio::test]
632 async fn test_early_sdp_stored_in_call_state() {
633 let (event_tx, _event_rx) = tokio::sync::broadcast::channel(16);
634 let media_stream = Arc::new(
635 MediaStreamBuilder::new(event_tx.clone())
636 .with_id("test-stream".to_string())
637 .build(),
638 );
639 let call_state: ActiveCallStateRef = Arc::new(RwLock::new(ActiveCallState::default()));
640 let cancel_token = CancellationToken::new();
641
642 let mut states = InviteDialogStates {
643 is_client: true,
644 session_id: "test-session".to_string(),
645 track_id: "test-track".to_string(),
646 cancel_token: cancel_token.clone(),
647 event_sender: event_tx.clone(),
648 call_state: call_state.clone(),
649 media_stream: media_stream.clone(),
650 terminated_reason: None,
651 has_early_media: false,
652 };
653
654 let early_resp = make_response_with_body(EARLY_MEDIA_SDP.as_bytes().to_vec());
656
657 let body = early_resp.body();
659 let answer = String::from_utf8_lossy(body);
660 let has_sdp = !answer.is_empty();
661 if states.is_client && has_sdp {
662 states.has_early_media = true;
663 {
664 let mut cs = states.call_state.write().await;
665 if cs.answer.is_none() {
666 cs.answer = Some(answer.to_string());
667 }
668 }
669 }
671
672 {
674 let cs = call_state.read().await;
675 assert!(
676 cs.answer.is_some(),
677 "call_state.answer should be set after 183 with SDP"
678 );
679 assert_eq!(
680 cs.answer.as_deref().unwrap(),
681 EARLY_MEDIA_SDP,
682 "call_state.answer should contain the early SDP"
683 );
684 }
685 assert!(states.has_early_media, "has_early_media should be true");
686 }
687
688 #[tokio::test]
695 async fn test_confirmed_empty_body_keeps_early_sdp() {
696 let (event_tx, _event_rx) = tokio::sync::broadcast::channel(16);
697 let media_stream = Arc::new(
698 MediaStreamBuilder::new(event_tx.clone())
699 .with_id("test-stream-2".to_string())
700 .build(),
701 );
702 let call_state: ActiveCallStateRef = Arc::new(RwLock::new(ActiveCallState::default()));
703 let cancel_token = CancellationToken::new();
704
705 let mut states = InviteDialogStates {
706 is_client: true,
707 session_id: "test-session-2".to_string(),
708 track_id: "test-track-2".to_string(),
709 cancel_token: cancel_token.clone(),
710 event_sender: event_tx.clone(),
711 call_state: call_state.clone(),
712 media_stream: media_stream.clone(),
713 terminated_reason: None,
714 has_early_media: false,
715 };
716
717 {
719 let answer_str = EARLY_MEDIA_SDP.to_string();
720 states.has_early_media = true;
721 let mut cs = states.call_state.write().await;
722 if cs.answer.is_none() {
723 cs.answer = Some(answer_str);
724 }
725 }
726
727 let confirmed_resp = make_response_with_body(vec![]); {
730 let mut cs = states.call_state.write().await;
731 cs.answer_time.replace(chrono::Utc::now());
732 cs.last_status_code = 200;
733 }
734 let body = confirmed_resp.body();
737 let answer = String::from_utf8_lossy(body);
738 let answer_trimmed = answer.trim();
739 if states.is_client && !answer_trimmed.is_empty() {
741 panic!("Confirmed handler should not update SDP for empty body");
744 }
745
746 {
748 let cs = call_state.read().await;
749 assert!(
750 cs.answer.is_some(),
751 "call_state.answer must not be None after 200 OK with empty body"
752 );
753 let stored_answer = cs.answer.as_deref().unwrap();
754 assert!(
755 !stored_answer.is_empty(),
756 "call_state.answer must not be empty after 200 OK with empty body"
757 );
758 assert_eq!(
759 stored_answer, EARLY_MEDIA_SDP,
760 "call_state.answer should still be the early SDP after 200 OK with empty body"
761 );
762 }
763 }
764
765 #[tokio::test]
772 async fn test_answer_fallback_to_early_sdp_when_200ok_empty() {
773 let call_state: ActiveCallStateRef = Arc::new(RwLock::new(ActiveCallState::default()));
775
776 {
778 let mut cs = call_state.write().await;
779 cs.answer = Some(EARLY_MEDIA_SDP.to_string());
780 }
781
782 let raw_answer: Option<Vec<u8>> = Some(vec![]); let resolved_answer = match raw_answer {
787 Some(bytes) => {
788 let s = String::from_utf8_lossy(&bytes).to_string();
789 if s.trim().is_empty() {
790 let cs = call_state.read().await;
792 match cs.answer.clone() {
793 Some(early_sdp) if !early_sdp.is_empty() => {
794 (early_sdp, true )
795 }
796 _ => (s, false),
797 }
798 } else {
799 (s, false)
800 }
801 }
802 None => {
803 let cs = call_state.read().await;
804 match cs.answer.clone() {
805 Some(early_sdp) if !early_sdp.is_empty() => (early_sdp, true),
806 _ => panic!("Expected early SDP fallback"),
807 }
808 }
809 };
810
811 let (answer, already_applied) = resolved_answer;
812
813 assert!(
816 !answer.is_empty(),
817 "Resolved answer must not be empty — should contain the early SDP"
818 );
819 assert_eq!(
820 answer, EARLY_MEDIA_SDP,
821 "Resolved answer should be the early SDP from the 183 handler"
822 );
823 assert!(
824 already_applied,
825 "remote_description_already_applied should be true when using early SDP fallback"
826 );
827 }
828
829 #[tokio::test]
833 async fn test_answer_uses_200ok_sdp_when_present() {
834 const FINAL_SDP: &str = "v=0\r\n\
835 o=- 2000 2 IN IP4 10.0.0.1\r\n\
836 s=SIP Call\r\n\
837 t=0 0\r\n\
838 m=audio 20000 RTP/AVP 0\r\n\
839 c=IN IP4 10.0.0.1\r\n\
840 a=rtpmap:0 PCMU/8000\r\n\
841 a=sendrecv\r\n";
842
843 let call_state: ActiveCallStateRef = Arc::new(RwLock::new(ActiveCallState::default()));
844
845 {
847 let mut cs = call_state.write().await;
848 cs.answer = Some(EARLY_MEDIA_SDP.to_string());
849 }
850
851 let raw_answer: Option<Vec<u8>> = Some(FINAL_SDP.as_bytes().to_vec());
852
853 let resolved_answer = match raw_answer {
854 Some(bytes) => {
855 let s = String::from_utf8_lossy(&bytes).to_string();
856 if s.trim().is_empty() {
857 let cs = call_state.read().await;
858 match cs.answer.clone() {
859 Some(early_sdp) if !early_sdp.is_empty() => (early_sdp, true),
860 _ => (s, false),
861 }
862 } else {
863 (s, false) }
865 }
866 None => panic!("Unexpected"),
867 };
868
869 let (answer, already_applied) = resolved_answer;
870
871 assert_eq!(
872 answer, FINAL_SDP,
873 "When 200 OK has SDP, it should be used (not the early SDP)"
874 );
875 assert!(
876 !already_applied,
877 "remote_description_already_applied should be false when 200 OK has SDP body"
878 );
879 }
880}