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