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 let is_refer = states.call_state.read().await.is_refer;
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 refer: Some(is_refer),
286 })?;
287 }
288 }
289 tx_handle.reply(rsipstack::rsip::StatusCode::OK).await.ok();
290 }
291 DialogState::Updated(dialog_id, _req, tx_handle) => {
292 info!(session_id = states.session_id, %dialog_id, "dialog update received");
293 let is_refer = states.call_state.read().await.is_refer;
294 let mut answer_sdp = None;
295 if let Some(sdp_body) = _req.body().get(..) {
296 let sdp_str = String::from_utf8_lossy(sdp_body);
297 if !sdp_str.is_empty()
298 && (_req.method == rsipstack::rsip::Method::Invite
299 || _req.method == rsipstack::rsip::Method::Update)
300 {
301 info!(session_id=states.session_id, %dialog_id, method=%_req.method, "handling re-invite/update offer");
302
303 let is_on_hold =
305 crate::media::negotiate::detect_hold_state_from_sdp(&sdp_str);
306 info!(session_id=states.session_id, %dialog_id, is_on_hold=%is_on_hold, "detected hold state from re-invite SDP");
307
308 if is_on_hold {
310 states
311 .media_stream
312 .hold_track(Some(states.track_id.clone()))
313 .await;
314 } else {
315 states
316 .media_stream
317 .resume_track(Some(states.track_id.clone()))
318 .await;
319 }
320
321 states
323 .event_sender
324 .send(crate::event::SessionEvent::Hold {
325 track_id: states.track_id.clone(),
326 timestamp: crate::media::get_timestamp(),
327 on_hold: is_on_hold,
328 refer: Some(is_refer),
329 })
330 .ok();
331
332 match states
333 .media_stream
334 .handshake(&states.track_id, sdp_str.to_string(), None)
335 .await
336 {
337 Ok(sdp) => answer_sdp = Some(sdp),
338 Err(e) => {
339 warn!(
340 session_id = states.session_id,
341 "failed to handle re-invite: {}", e
342 );
343 }
344 }
345 } else {
346 info!(session_id=states.session_id, %dialog_id, "updating remote description:\n{}", sdp_str);
347
348 let is_on_hold =
350 crate::media::negotiate::detect_hold_state_from_sdp(&sdp_str);
351 if is_on_hold {
352 states
353 .media_stream
354 .hold_track(Some(states.track_id.clone()))
355 .await;
356 states
357 .event_sender
358 .send(crate::event::SessionEvent::Hold {
359 track_id: states.track_id.clone(),
360 timestamp: crate::media::get_timestamp(),
361 on_hold: true,
362 refer: Some(is_refer),
363 })
364 .ok();
365 } else {
366 states
367 .media_stream
368 .resume_track(Some(states.track_id.clone()))
369 .await;
370 states
371 .event_sender
372 .send(crate::event::SessionEvent::Hold {
373 track_id: states.track_id.clone(),
374 timestamp: crate::media::get_timestamp(),
375 on_hold: false,
376 refer: Some(is_refer),
377 })
378 .ok();
379 }
380
381 states
382 .media_stream
383 .update_remote_description(&states.track_id, &sdp_str.to_string())
384 .await?;
385 }
386 }
387
388 if let Some(sdp) = answer_sdp {
389 tx_handle
390 .respond(
391 rsipstack::rsip::StatusCode::OK,
392 Some(vec![rsipstack::rsip::Header::ContentType(
393 "application/sdp".to_string().into(),
394 )]),
395 Some(sdp.into()),
396 )
397 .await
398 .ok();
399 } else {
400 tx_handle.reply(rsipstack::rsip::StatusCode::OK).await.ok();
401 }
402 }
403 DialogState::Options(dialog_id, _req, tx_handle) => {
404 info!(session_id = states.session_id, %dialog_id, "dialog options received");
405 tx_handle.reply(rsipstack::rsip::StatusCode::OK).await.ok();
406 }
407 DialogState::Refer(dialog_id, req, tx_handle) => {
408 let refer_to = req.headers.iter().find_map(|h| {
409 if let rsipstack::rsip::Header::ReferTo(h) = h {
410 return Some(h.value().to_string());
411 }
412 None
413 }).unwrap_or_default();
414 let referred_by = req.headers.iter().find_map(|h| {
415 if let rsipstack::rsip::Header::ReferredBy(h) = h {
416 return Some(h.value().to_string());
417 }
418 None
419 });
420 info!(session_id = states.session_id, %dialog_id, %refer_to, "received REFER");
421 tx_handle.reply(rsipstack::rsip::StatusCode::Other(202, "Accepted".into())).await.ok();
422 let is_refer = states.call_state.read().await.is_refer;
423 states.event_sender.send(crate::event::SessionEvent::TransferRequest {
424 track_id: states.track_id.clone(),
425 timestamp: crate::media::get_timestamp(),
426 refer_to,
427 referred_by,
428 refer: Some(is_refer),
429 }).ok();
430 }
431 DialogState::Terminated(dialog_id, reason) => {
432 info!(
433 session_id = states.session_id,
434 ?dialog_id,
435 ?reason,
436 "dialog terminated"
437 );
438 states.terminated_reason = Some(reason.clone());
439 return Ok(());
440 }
441 other_state => {
442 info!(
443 session_id = states.session_id,
444 %other_state,
445 "dialog received other state"
446 );
447 }
448 }
449 }
450 Ok(())
451 }
452
453 pub(super) async fn process_dialog(&mut self, mut states: InviteDialogStates) {
454 let token = states.cancel_token.clone();
455 tokio::select! {
456 _ = token.cancelled() => {
457 states.terminated_reason = Some(TerminatedReason::UacCancel);
458 }
459 _ = self.dialog_event_loop(&mut states) => {}
460 };
461
462 {
464 let state = states.call_state.read().await;
465 if let Some(extras) = &state.extras {
466 if let Some(h_val) = extras.get("_hangup_headers") {
467 if let Ok(headers_map) =
468 serde_json::from_value::<HashMap<String, String>>(h_val.clone())
469 {
470 let mut headers = Vec::new();
471 for (k, v) in headers_map {
472 headers.push(rsipstack::rsip::Header::Other(k.into(), v.into()));
473 }
474 if !headers.is_empty() {
475 if let Some(existing) = &mut self.hangup_headers {
476 existing.extend(headers);
477 } else {
478 self.hangup_headers = Some(headers);
479 }
480 }
481 }
482 }
483 }
484 }
485
486 self.drop_async().await;
487 }
488}
489
490#[derive(Clone)]
491pub struct Invitation {
492 pub dialog_layer: Arc<DialogLayer>,
493 pub pending_dialogs: Arc<std::sync::Mutex<HashMap<DialogId, PendingDialog>>>,
494}
495
496impl Invitation {
497 pub fn new(dialog_layer: Arc<DialogLayer>) -> Self {
498 Self {
499 dialog_layer,
500 pending_dialogs: Arc::new(std::sync::Mutex::new(HashMap::new())),
501 }
502 }
503
504 pub fn add_pending(&self, dialog_id: DialogId, pending: PendingDialog) {
505 self.pending_dialogs
506 .lock()
507 .map(|mut ps| ps.insert(dialog_id, pending))
508 .ok();
509 }
510
511 pub fn get_pending_call(&self, dialog_id: &DialogId) -> Option<PendingDialog> {
512 self.pending_dialogs
513 .lock()
514 .ok()
515 .and_then(|mut ps| ps.remove(dialog_id))
516 }
517
518 pub fn has_pending_call(&self, dialog_id: &DialogId) -> bool {
519 self.pending_dialogs
520 .lock()
521 .ok()
522 .map(|ps| ps.contains_key(dialog_id))
523 .unwrap_or(false)
524 }
525
526 pub fn find_dialog_id_by_session_id(&self, session_id: &str) -> Option<DialogId> {
527 self.pending_dialogs.lock().ok().and_then(|ps| {
528 ps.iter()
529 .find(|(id, _)| id.to_string() == session_id)
530 .map(|(id, _)| id.clone())
531 })
532 }
533
534 pub async fn hangup(
535 &self,
536 dialog_id: DialogId,
537 code: Option<rsipstack::rsip::StatusCode>,
538 reason: Option<String>,
539 ) -> Result<()> {
540 if let Some(call) = self.get_pending_call(&dialog_id) {
541 call.dialog.reject(code, reason).ok();
542 }
543 match self.dialog_layer.get_dialog(&dialog_id) {
544 Some(dialog) => {
545 self.dialog_layer.remove_dialog(&dialog_id);
546 dialog.hangup().await.ok();
547 }
548 None => {}
549 }
550 Ok(())
551 }
552
553 pub async fn reject(&self, dialog_id: DialogId) -> Result<()> {
554 if let Some(call) = self.get_pending_call(&dialog_id) {
555 call.dialog.reject(None, None).ok();
556 }
557 match self.dialog_layer.get_dialog(&dialog_id) {
558 Some(dialog) => {
559 self.dialog_layer.remove_dialog(&dialog_id);
560 dialog.hangup().await.ok();
561 }
562 None => {}
563 }
564 Ok(())
565 }
566
567 pub async fn invite(
568 &self,
569 invite_option: InviteOption,
570 state_sender: DialogStateSender,
571 ) -> Result<(DialogId, Option<Vec<u8>>), rsipstack::Error> {
572 let (dialog, resp) = self
573 .dialog_layer
574 .do_invite(invite_option, state_sender)
575 .await?;
576
577 let offer = match resp {
578 Some(resp) => match resp.status_code.kind() {
579 rsipstack::rsip::StatusCodeKind::Successful => {
580 let offer = resp.body.clone();
581 Some(offer)
582 }
583 _ => {
584 let reason = resp
585 .reason_phrase()
586 .unwrap_or(&resp.status_code.to_string())
587 .to_string();
588 return Err(rsipstack::Error::DialogError(
589 reason,
590 dialog.id(),
591 resp.status_code,
592 ));
593 }
594 },
595 None => {
596 return Err(rsipstack::Error::DialogError(
597 "no response received".to_string(),
598 dialog.id(),
599 rsipstack::rsip::StatusCode::NotAcceptableHere,
600 ));
601 }
602 };
603 Ok((dialog.id(), offer))
604 }
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use crate::call::active_call::ActiveCallState;
611 use crate::media::stream::MediaStreamBuilder;
612 use std::sync::Arc;
613 use tokio::sync::RwLock;
614 use tokio_util::sync::CancellationToken;
615
616 const EARLY_MEDIA_SDP: &str = "v=0\r\n\
618 o=- 1000 1 IN IP4 192.168.1.100\r\n\
619 s=SIP Call\r\n\
620 t=0 0\r\n\
621 m=audio 10000 RTP/AVP 0\r\n\
622 c=IN IP4 192.168.1.100\r\n\
623 a=rtpmap:0 PCMU/8000\r\n\
624 a=sendrecv\r\n";
625
626 fn make_response_with_body(body: Vec<u8>) -> rsipstack::rsip::Response {
627 let mut resp = rsipstack::rsip::Response::default();
628 resp.body = body;
629 resp
630 }
631
632 #[tokio::test]
636 async fn test_early_sdp_stored_in_call_state() {
637 let (event_tx, _event_rx) = tokio::sync::broadcast::channel(16);
638 let media_stream = Arc::new(
639 MediaStreamBuilder::new(event_tx.clone())
640 .with_id("test-stream".to_string())
641 .build(),
642 );
643 let call_state: ActiveCallStateRef = Arc::new(RwLock::new(ActiveCallState::default()));
644 let cancel_token = CancellationToken::new();
645
646 let mut states = InviteDialogStates {
647 is_client: true,
648 session_id: "test-session".to_string(),
649 track_id: "test-track".to_string(),
650 cancel_token: cancel_token.clone(),
651 event_sender: event_tx.clone(),
652 call_state: call_state.clone(),
653 media_stream: media_stream.clone(),
654 terminated_reason: None,
655 has_early_media: false,
656 };
657
658 let early_resp = make_response_with_body(EARLY_MEDIA_SDP.as_bytes().to_vec());
660
661 let body = early_resp.body();
663 let answer = String::from_utf8_lossy(body);
664 let has_sdp = !answer.is_empty();
665 if states.is_client && has_sdp {
666 states.has_early_media = true;
667 {
668 let mut cs = states.call_state.write().await;
669 if cs.answer.is_none() {
670 cs.answer = Some(answer.to_string());
671 }
672 }
673 }
675
676 {
678 let cs = call_state.read().await;
679 assert!(
680 cs.answer.is_some(),
681 "call_state.answer should be set after 183 with SDP"
682 );
683 assert_eq!(
684 cs.answer.as_deref().unwrap(),
685 EARLY_MEDIA_SDP,
686 "call_state.answer should contain the early SDP"
687 );
688 }
689 assert!(states.has_early_media, "has_early_media should be true");
690 }
691
692 #[tokio::test]
699 async fn test_confirmed_empty_body_keeps_early_sdp() {
700 let (event_tx, _event_rx) = tokio::sync::broadcast::channel(16);
701 let media_stream = Arc::new(
702 MediaStreamBuilder::new(event_tx.clone())
703 .with_id("test-stream-2".to_string())
704 .build(),
705 );
706 let call_state: ActiveCallStateRef = Arc::new(RwLock::new(ActiveCallState::default()));
707 let cancel_token = CancellationToken::new();
708
709 let mut states = InviteDialogStates {
710 is_client: true,
711 session_id: "test-session-2".to_string(),
712 track_id: "test-track-2".to_string(),
713 cancel_token: cancel_token.clone(),
714 event_sender: event_tx.clone(),
715 call_state: call_state.clone(),
716 media_stream: media_stream.clone(),
717 terminated_reason: None,
718 has_early_media: false,
719 };
720
721 {
723 let answer_str = EARLY_MEDIA_SDP.to_string();
724 states.has_early_media = true;
725 let mut cs = states.call_state.write().await;
726 if cs.answer.is_none() {
727 cs.answer = Some(answer_str);
728 }
729 }
730
731 let confirmed_resp = make_response_with_body(vec![]); {
734 let mut cs = states.call_state.write().await;
735 cs.answer_time.replace(chrono::Utc::now());
736 cs.last_status_code = 200;
737 }
738 let body = confirmed_resp.body();
741 let answer = String::from_utf8_lossy(body);
742 let answer_trimmed = answer.trim();
743 if states.is_client && !answer_trimmed.is_empty() {
745 panic!("Confirmed handler should not update SDP for empty body");
748 }
749
750 {
752 let cs = call_state.read().await;
753 assert!(
754 cs.answer.is_some(),
755 "call_state.answer must not be None after 200 OK with empty body"
756 );
757 let stored_answer = cs.answer.as_deref().unwrap();
758 assert!(
759 !stored_answer.is_empty(),
760 "call_state.answer must not be empty after 200 OK with empty body"
761 );
762 assert_eq!(
763 stored_answer, EARLY_MEDIA_SDP,
764 "call_state.answer should still be the early SDP after 200 OK with empty body"
765 );
766 }
767 }
768
769 #[tokio::test]
776 async fn test_answer_fallback_to_early_sdp_when_200ok_empty() {
777 let call_state: ActiveCallStateRef = Arc::new(RwLock::new(ActiveCallState::default()));
779
780 {
782 let mut cs = call_state.write().await;
783 cs.answer = Some(EARLY_MEDIA_SDP.to_string());
784 }
785
786 let raw_answer: Option<Vec<u8>> = Some(vec![]); let resolved_answer = match raw_answer {
791 Some(bytes) => {
792 let s = String::from_utf8_lossy(&bytes).to_string();
793 if s.trim().is_empty() {
794 let cs = call_state.read().await;
796 match cs.answer.clone() {
797 Some(early_sdp) if !early_sdp.is_empty() => {
798 (early_sdp, true )
799 }
800 _ => (s, false),
801 }
802 } else {
803 (s, false)
804 }
805 }
806 None => {
807 let cs = call_state.read().await;
808 match cs.answer.clone() {
809 Some(early_sdp) if !early_sdp.is_empty() => (early_sdp, true),
810 _ => panic!("Expected early SDP fallback"),
811 }
812 }
813 };
814
815 let (answer, already_applied) = resolved_answer;
816
817 assert!(
820 !answer.is_empty(),
821 "Resolved answer must not be empty — should contain the early SDP"
822 );
823 assert_eq!(
824 answer, EARLY_MEDIA_SDP,
825 "Resolved answer should be the early SDP from the 183 handler"
826 );
827 assert!(
828 already_applied,
829 "remote_description_already_applied should be true when using early SDP fallback"
830 );
831 }
832
833 #[tokio::test]
837 async fn test_answer_uses_200ok_sdp_when_present() {
838 const FINAL_SDP: &str = "v=0\r\n\
839 o=- 2000 2 IN IP4 10.0.0.1\r\n\
840 s=SIP Call\r\n\
841 t=0 0\r\n\
842 m=audio 20000 RTP/AVP 0\r\n\
843 c=IN IP4 10.0.0.1\r\n\
844 a=rtpmap:0 PCMU/8000\r\n\
845 a=sendrecv\r\n";
846
847 let call_state: ActiveCallStateRef = Arc::new(RwLock::new(ActiveCallState::default()));
848
849 {
851 let mut cs = call_state.write().await;
852 cs.answer = Some(EARLY_MEDIA_SDP.to_string());
853 }
854
855 let raw_answer: Option<Vec<u8>> = Some(FINAL_SDP.as_bytes().to_vec());
856
857 let resolved_answer = match raw_answer {
858 Some(bytes) => {
859 let s = String::from_utf8_lossy(&bytes).to_string();
860 if s.trim().is_empty() {
861 let cs = call_state.read().await;
862 match cs.answer.clone() {
863 Some(early_sdp) if !early_sdp.is_empty() => (early_sdp, true),
864 _ => (s, false),
865 }
866 } else {
867 (s, false) }
869 }
870 None => panic!("Unexpected"),
871 };
872
873 let (answer, already_applied) = resolved_answer;
874
875 assert_eq!(
876 answer, FINAL_SDP,
877 "When 200 OK has SDP, it should be used (not the early SDP)"
878 );
879 assert!(
880 !already_applied,
881 "remote_description_already_applied should be false when 200 OK has SDP body"
882 );
883 }
884}