1use crate::call::state::{CallProgress, LegShared};
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(crate) fn remove_dialog(layer: &DialogLayer, id: &DialogId) -> Option<Dialog> {
23 let dialog = layer.get_dialog(id)?;
24 layer.remove_dialog(id);
25 Some(dialog)
26}
27
28pub struct DialogStateReceiverGuard {
29 pub(super) dialog_layer: Arc<DialogLayer>,
30 pub(super) receiver: DialogStateReceiver,
31 pub(super) dialog_id: Option<DialogId>,
32 pub(super) hangup_headers: Option<Vec<rsipstack::rsip::Header>>,
33}
34
35impl DialogStateReceiverGuard {
36 pub fn new(
37 dialog_layer: Arc<DialogLayer>,
38 receiver: DialogStateReceiver,
39 hangup_headers: Option<Vec<rsipstack::rsip::Header>>,
40 ) -> Self {
41 Self {
42 dialog_layer,
43 receiver,
44 dialog_id: None,
45 hangup_headers,
46 }
47 }
48 pub async fn recv(&mut self) -> Option<DialogState> {
49 let state = self.receiver.recv().await;
50 if let Some(ref s) = state {
51 self.dialog_id = Some(s.id().clone());
52 }
53 state
54 }
55
56 fn take_dialog(&mut self) -> Option<Dialog> {
57 let id = self.dialog_id.take()?;
58 info!(%id, "dialog removed on drop");
59 if let Some(dialog) = remove_dialog(&self.dialog_layer, &id) {
60 return Some(dialog);
61 }
62 self.dialog_layer
70 .get_client_dialog_by_call_id(&id.call_id)
71 .into_iter()
72 .next()
73 .map(Dialog::Invite)
74 }
75
76 pub async fn drop_async(&mut self) {
77 if let Some(dialog) = self.take_dialog() {
78 if let Err(e) = dialog.hangup_with_headers(self.hangup_headers.take()).await {
79 warn!(id=%dialog.id(), "error hanging up dialog on drop: {}", e);
80 }
81 }
82 }
83}
84
85impl Drop for DialogStateReceiverGuard {
86 fn drop(&mut self) {
87 if let Some(dialog) = self.take_dialog() {
88 crate::spawn(async move {
89 if let Err(e) = dialog.hangup().await {
90 warn!(id=%dialog.id(), "error hanging up dialog on drop: {}", e);
91 }
92 });
93 }
94 }
95}
96
97pub(super) struct InviteDialogStates {
98 pub is_client: bool,
99 pub session_id: String,
100 pub track_id: TrackId,
101 pub cancel_token: CancellationToken,
102 pub event_sender: EventSender,
103 pub leg: LegShared,
105 pub media_stream: Arc<MediaStream>,
106 pub terminated_reason: Option<TerminatedReason>,
107 pub has_early_media: bool,
108 initial_confirmed: bool,
116 pub hangup_reason: Option<CallRecordHangupReason>,
119}
120
121impl InviteDialogStates {
122 pub(super) fn new(
123 is_client: bool,
124 session_id: String,
125 track_id: TrackId,
126 event_sender: EventSender,
127 media_stream: Arc<MediaStream>,
128 leg: LegShared,
129 cancel_token: CancellationToken,
130 hangup_reason: Option<CallRecordHangupReason>,
131 ) -> Self {
132 Self {
133 is_client,
134 session_id,
135 track_id,
136 cancel_token,
137 event_sender,
138 leg,
139 media_stream,
140 terminated_reason: None,
141 has_early_media: false,
142 initial_confirmed: false,
143 hangup_reason,
144 }
145 }
146}
147
148impl InviteDialogStates {
149 pub(super) fn on_terminated(&mut self) {
153 let term = CallProgress::termination(self.terminated_reason.as_ref());
154 let status_code = term.status_code;
155 let reason = term.hangup_reason;
156 self.leg.update_progress(|p| {
157 p.last_status_code = status_code;
158 p.set_hangup_reason(reason.clone());
159 });
160 let progress = self.leg.progress.load_full();
161
162 self.event_sender
163 .send(crate::event::SessionEvent::TrackEnd {
164 track_id: self.track_id.clone(),
165 timestamp: crate::media::get_timestamp(),
166 duration: progress
167 .answer_time
168 .map(|t| (Utc::now() - t).num_milliseconds())
169 .unwrap_or_default() as u64,
170 ssrc: self.leg.ssrc,
171 play_id: None,
172 auto_hangup: self.hangup_reason.clone(),
173 })
174 .ok();
175 let hangup_event = self
176 .leg
177 .build_hangup_event(self.track_id.clone(), Some(term.initiator.to_string()));
178 self.event_sender.send(hangup_event).ok();
179 }
180}
181
182impl Drop for InviteDialogStates {
183 fn drop(&mut self) {
184 self.on_terminated();
185 self.cancel_token.cancel();
186 }
187}
188
189impl DialogStateReceiverGuard {
190 pub(self) async fn dialog_event_loop(&mut self, states: &mut InviteDialogStates) -> Result<()> {
191 while let Some(event) = self.recv().await {
192 match event {
193 DialogState::Calling(dialog_id) => {
194 info!(session_id=states.session_id, %dialog_id, "dialog calling");
195 states
196 .leg
197 .update_progress(|p| p.session_id = dialog_id.to_string());
198 }
199 DialogState::Trying(_) => {}
200 DialogState::Early(dialog_id, resp) => {
201 let code = resp.status_code.code();
202 let body = resp.body();
203 let answer = String::from_utf8_lossy(body);
204 let has_sdp = !answer.is_empty();
205 info!(session_id=states.session_id, %dialog_id, has_sdp=%has_sdp, "dialog early ({}): \n{}", code, answer);
206
207 states.leg.update_progress(|p| p.on_early(code));
208
209 if !states.is_client {
210 continue;
211 }
212
213 states
214 .event_sender
215 .send(crate::event::SessionEvent::Ringing {
216 track_id: states.track_id.clone(),
217 timestamp: crate::media::get_timestamp(),
218 early_media: has_sdp,
219 refer: Some(states.leg.is_refer),
220 })?;
221
222 if has_sdp {
223 states.has_early_media = true;
224 states.leg.update_progress(|p| p.try_set_answer(&answer));
225 states
226 .media_stream
227 .update_remote_description_provisional(
228 &states.track_id,
229 &answer.to_string(),
230 )
231 .await?;
232 }
233 }
234 DialogState::Confirmed(dialog_id, msg) => {
235 info!(session_id=states.session_id, %dialog_id, has_early_media=%states.has_early_media, "dialog confirmed");
236 states
237 .leg
238 .update_progress(|p| p.on_confirmed(dialog_id.to_string()));
239 if states.is_client && !states.initial_confirmed {
240 states.initial_confirmed = true;
241 let answer = String::from_utf8_lossy(msg.body());
242 let answer = answer.trim();
243 if !answer.is_empty() {
244 if states.has_early_media {
245 info!(
246 session_id = states.session_id,
247 "updating remote description with final answer after early media (force=true)"
248 );
249 if let Err(e) = states
252 .media_stream
253 .update_remote_description_force(
254 &states.track_id,
255 &answer.to_string(),
256 )
257 .await
258 {
259 tracing::warn!(
260 session_id = states.session_id,
261 "failed to force update remote description on confirmed: {}",
262 e
263 );
264 }
265 } else {
266 if let Err(e) = states
267 .media_stream
268 .update_remote_description(
269 &states.track_id,
270 &answer.to_string(),
271 )
272 .await
273 {
274 tracing::warn!(
275 session_id = states.session_id,
276 "failed to update remote description on confirmed: {}",
277 e
278 );
279 }
280 }
281 }
282 }
283 }
284 DialogState::Info(dialog_id, req, tx_handle) => {
285 let body_str = String::from_utf8_lossy(req.body());
286 info!(session_id=states.session_id, %dialog_id, body=%body_str, "dialog info received");
287 if body_str.starts_with("Signal=") {
288 let digit = body_str.trim_start_matches("Signal=").chars().next();
289 if let Some(digit) = digit {
290 states.event_sender.send(crate::event::SessionEvent::Dtmf {
291 track_id: states.track_id.clone(),
292 timestamp: crate::media::get_timestamp(),
293 digit: digit.to_string(),
294 refer: Some(states.leg.is_refer),
295 })?;
296 }
297 }
298 tx_handle.reply(rsipstack::rsip::StatusCode::OK).await.ok();
299 }
300 DialogState::Message(dialog_id, req, tx_handle) => {
301 let body_str = String::from_utf8_lossy(req.body()).to_string();
302 let content_type = req.headers.iter().find_map(|h| {
303 if let rsipstack::rsip::Header::ContentType(content_type) = h {
304 Some(content_type.value().to_string())
305 } else {
306 None
307 }
308 });
309 info!(
310 session_id=states.session_id,
311 %dialog_id,
312 content_type=content_type.as_deref(),
313 body=%body_str,
314 "dialog message received"
315 );
316 states
317 .event_sender
318 .send(crate::event::SessionEvent::Message {
319 track_id: states.track_id.clone(),
320 timestamp: crate::media::get_timestamp(),
321 body: body_str,
322 content_type,
323 refer: Some(states.leg.is_refer),
324 })
325 .ok();
326 tx_handle.reply(rsipstack::rsip::StatusCode::OK).await.ok();
327 }
328 DialogState::Updated(dialog_id, _req, tx_handle) => {
329 info!(session_id = states.session_id, %dialog_id, "dialog update received");
330 let mut answer_sdp = None;
331 if let Some(sdp_body) = _req.body().get(..) {
332 let sdp_str = String::from_utf8_lossy(sdp_body);
333 if !sdp_str.is_empty()
334 && (_req.method == rsipstack::rsip::Method::Invite
335 || _req.method == rsipstack::rsip::Method::Update)
336 {
337 info!(session_id=states.session_id, %dialog_id, method=%_req.method, "handling re-invite/update offer");
338
339 let is_on_hold =
341 crate::media::negotiate::detect_hold_state_from_sdp(&sdp_str);
342 info!(session_id=states.session_id, %dialog_id, is_on_hold=%is_on_hold, "detected hold state from re-invite SDP");
343
344 apply_hold_state(states, is_on_hold).await;
346
347 match states
348 .media_stream
349 .handshake(&states.track_id, sdp_str.to_string(), None)
350 .await
351 {
352 Ok(sdp) => answer_sdp = Some(sdp),
353 Err(e) => {
354 warn!(
355 session_id = states.session_id,
356 "failed to handle re-invite: {}", e
357 );
358 }
359 }
360 } else {
361 info!(session_id=states.session_id, %dialog_id, "updating remote description:\n{}", sdp_str);
362
363 let is_on_hold =
365 crate::media::negotiate::detect_hold_state_from_sdp(&sdp_str);
366 apply_hold_state(states, is_on_hold).await;
367
368 states
369 .media_stream
370 .update_remote_description(&states.track_id, &sdp_str.to_string())
371 .await?;
372 }
373 }
374
375 if let Some(sdp) = answer_sdp {
376 tx_handle
377 .respond(
378 rsipstack::rsip::StatusCode::OK,
379 Some(vec![rsipstack::rsip::Header::ContentType(
380 "application/sdp".to_string().into(),
381 )]),
382 Some(sdp.into()),
383 )
384 .await
385 .ok();
386 } else {
387 tx_handle.reply(rsipstack::rsip::StatusCode::OK).await.ok();
388 }
389 }
390 DialogState::Options(dialog_id, _req, tx_handle) => {
391 info!(session_id = states.session_id, %dialog_id, "dialog options received");
392 tx_handle.reply(rsipstack::rsip::StatusCode::OK).await.ok();
393 }
394 DialogState::Refer(dialog_id, req, tx_handle) => {
395 let refer_to = req
396 .headers
397 .iter()
398 .find_map(|h| {
399 if let rsipstack::rsip::Header::ReferTo(h) = h {
400 return Some(h.value().to_string());
401 }
402 None
403 })
404 .unwrap_or_default();
405 let referred_by = req.headers.iter().find_map(|h| {
406 if let rsipstack::rsip::Header::ReferredBy(h) = h {
407 return Some(h.value().to_string());
408 }
409 None
410 });
411 info!(session_id = states.session_id, %dialog_id, %refer_to, "received REFER");
412 tx_handle
413 .reply(rsipstack::rsip::StatusCode::Other(202, "Accepted".into()))
414 .await
415 .ok();
416 states
417 .event_sender
418 .send(crate::event::SessionEvent::TransferRequest {
419 track_id: states.track_id.clone(),
420 timestamp: crate::media::get_timestamp(),
421 refer_to,
422 referred_by,
423 refer: Some(states.leg.is_refer),
424 })
425 .ok();
426 }
427 DialogState::Terminated(dialog_id, reason) => {
428 info!(
429 session_id = states.session_id,
430 ?dialog_id,
431 ?reason,
432 "dialog terminated"
433 );
434 states.terminated_reason = Some(reason.clone());
435 return Ok(());
436 }
437 other_state => {
438 info!(
439 session_id = states.session_id,
440 %other_state,
441 "dialog received other state"
442 );
443 }
444 }
445 }
446 Ok(())
447 }
448
449 pub(super) async fn process_dialog(&mut self, mut states: InviteDialogStates) {
450 let token = states.cancel_token.clone();
451 tokio::select! {
452 _ = token.cancelled() => {
453 states.terminated_reason = Some(TerminatedReason::UacCancel);
454 }
455 _ = self.dialog_event_loop(&mut states) => {}
456 };
457
458 let extras = states.leg.extras.load_full();
460 if let Some(headers) = crate::sip_util::hangup_headers_from_extras(&extras) {
461 match &mut self.hangup_headers {
462 Some(existing) => existing.extend(headers),
463 None => self.hangup_headers = Some(headers),
464 }
465 }
466
467 self.drop_async().await;
468 }
469}
470
471async fn apply_hold_state(states: &mut InviteDialogStates, is_on_hold: bool) {
473 if is_on_hold {
474 states
475 .media_stream
476 .hold_track(Some(states.track_id.clone()))
477 .await;
478 } else {
479 states
480 .media_stream
481 .resume_track(Some(states.track_id.clone()))
482 .await;
483 }
484 states
485 .event_sender
486 .send(crate::event::SessionEvent::Hold {
487 track_id: states.track_id.clone(),
488 timestamp: crate::media::get_timestamp(),
489 on_hold: is_on_hold,
490 refer: Some(states.leg.is_refer),
491 })
492 .ok();
493}
494
495#[derive(Clone)]
496pub struct Invitation {
497 pub dialog_layer: Arc<DialogLayer>,
498 pub pending_dialogs: Arc<std::sync::Mutex<HashMap<DialogId, PendingDialog>>>,
499}
500
501impl Invitation {
502 pub fn new(dialog_layer: Arc<DialogLayer>) -> Self {
503 Self {
504 dialog_layer,
505 pending_dialogs: Arc::new(std::sync::Mutex::new(HashMap::new())),
506 }
507 }
508
509 pub fn add_pending(&self, dialog_id: DialogId, pending: PendingDialog) {
510 self.pending_dialogs
511 .lock()
512 .map(|mut ps| ps.insert(dialog_id, pending))
513 .ok();
514 }
515
516 pub fn get_pending_call(&self, dialog_id: &DialogId) -> Option<PendingDialog> {
517 self.pending_dialogs
518 .lock()
519 .ok()
520 .and_then(|mut ps| ps.remove(dialog_id))
521 }
522
523 pub fn has_pending_call(&self, dialog_id: &DialogId) -> bool {
524 self.pending_dialogs
525 .lock()
526 .ok()
527 .map(|ps| ps.contains_key(dialog_id))
528 .unwrap_or(false)
529 }
530
531 pub fn find_dialog_id_by_session_id(&self, session_id: &str) -> Option<DialogId> {
532 self.pending_dialogs.lock().ok().and_then(|ps| {
533 ps.iter()
534 .find(|(id, _)| id.to_string() == session_id)
535 .map(|(id, _)| id.clone())
536 })
537 }
538
539 pub async fn hangup(
541 &self,
542 dialog_id: DialogId,
543 code: Option<rsipstack::rsip::StatusCode>,
544 reason: Option<String>,
545 ) -> Result<()> {
546 if let Some(call) = self.get_pending_call(&dialog_id) {
547 call.dialog.reject(code, reason).ok();
548 }
549 if let Some(dialog) = remove_dialog(&self.dialog_layer, &dialog_id) {
550 dialog.hangup().await.ok();
551 }
552 Ok(())
553 }
554
555 pub async fn invite(
556 &self,
557 invite_option: InviteOption,
558 state_sender: DialogStateSender,
559 ) -> Result<(DialogId, Option<Vec<u8>>), rsipstack::Error> {
560 let (dialog, resp) = self
561 .dialog_layer
562 .do_invite(invite_option, state_sender)
563 .await?;
564
565 let offer = match resp {
566 Some(resp) => match resp.status_code.kind() {
567 rsipstack::rsip::StatusCodeKind::Successful => {
568 let offer = resp.body.clone();
569 Some(offer)
570 }
571 _ => {
572 let reason = resp
573 .reason_phrase()
574 .unwrap_or(&resp.status_code.to_string())
575 .to_string();
576 return Err(rsipstack::Error::DialogError(
577 reason,
578 dialog.id(),
579 resp.status_code,
580 ));
581 }
582 },
583 None => {
584 return Err(rsipstack::Error::DialogError(
585 "no response received".to_string(),
586 dialog.id(),
587 rsipstack::rsip::StatusCode::NotAcceptableHere,
588 ));
589 }
590 };
591 Ok((dialog.id(), offer))
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598 use crate::call::state::CallProgress;
599 use crate::media::stream::MediaStreamBuilder;
600
601 const EARLY_MEDIA_SDP: &str = "v=0\r\n\
603 o=- 1000 1 IN IP4 192.168.1.100\r\n\
604 s=SIP Call\r\n\
605 t=0 0\r\n\
606 m=audio 10000 RTP/AVP 0\r\n\
607 c=IN IP4 192.168.1.100\r\n\
608 a=rtpmap:0 PCMU/8000\r\n\
609 a=sendrecv\r\n";
610
611 fn make_states(has_early_media: bool) -> InviteDialogStates {
612 let (event_tx, _event_rx) = tokio::sync::broadcast::channel(16);
613 let media_stream = Arc::new(
614 MediaStreamBuilder::new(event_tx.clone())
615 .with_id("test-stream".to_string())
616 .build(),
617 );
618 let cancel_token = CancellationToken::new();
619 let leg = LegShared::new(1000, true, CallProgress::default());
620
621 InviteDialogStates {
622 is_client: true,
623 session_id: "test-session".to_string(),
624 track_id: "test-track".to_string(),
625 cancel_token: cancel_token.clone(),
626 event_sender: event_tx.clone(),
627 leg,
628 media_stream,
629 terminated_reason: None,
630 has_early_media,
631 initial_confirmed: false,
632 hangup_reason: None,
633 }
634 }
635
636 #[tokio::test]
640 async fn test_early_sdp_stored_in_leg_progress() {
641 let mut states = make_states(false);
642
643 let answer = EARLY_MEDIA_SDP.to_string();
646 let has_sdp = !answer.is_empty();
647 if states.is_client && has_sdp {
648 states.has_early_media = true;
649 states.leg.update_progress(|p| p.try_set_answer(&answer));
650 }
651
652 let progress = states.leg.progress.load_full();
654 assert!(
655 progress.answer.is_some(),
656 "leg progress answer should be set after 183 with SDP"
657 );
658 assert_eq!(
659 progress.answer.as_deref().unwrap(),
660 EARLY_MEDIA_SDP,
661 "leg progress answer should contain the early SDP"
662 );
663 assert!(states.has_early_media, "has_early_media should be true");
664 }
665
666 #[tokio::test]
673 async fn test_confirmed_empty_body_keeps_early_sdp() {
674 let mut states = make_states(false);
675
676 states.has_early_media = true;
678 states
679 .leg
680 .update_progress(|p| p.try_set_answer(EARLY_MEDIA_SDP));
681
682 states
684 .leg
685 .update_progress(|p| p.on_confirmed("dialog-1".to_string()));
686 let confirmed_answer = String::new();
689 assert!(
690 confirmed_answer.trim().is_empty(),
691 "empty body must not be applied"
692 );
693
694 let progress = states.leg.progress.load_full();
696 assert!(
697 progress.answer.is_some(),
698 "answer must not be None after 200 OK with empty body"
699 );
700 let stored_answer = progress.answer.as_deref().unwrap();
701 assert!(
702 !stored_answer.is_empty(),
703 "answer must not be empty after 200 OK with empty body"
704 );
705 assert_eq!(
706 stored_answer, EARLY_MEDIA_SDP,
707 "answer should still be the early SDP after 200 OK with empty body"
708 );
709 }
710
711 #[tokio::test]
715 async fn test_answer_fallback_to_early_sdp_when_200ok_empty() {
716 let states = make_states(true);
718 states
719 .leg
720 .update_progress(|p| p.try_set_answer(EARLY_MEDIA_SDP));
721
722 let early = states.leg.progress.load_full().answer.clone();
724 let raw_answer: Option<Vec<u8>> = Some(vec![]); let (answer, already_applied) =
727 crate::call::state::resolve_final_answer(raw_answer, early.as_ref()).unwrap();
728
729 assert!(
732 !answer.is_empty(),
733 "Resolved answer must not be empty — should contain the early SDP"
734 );
735 assert_eq!(
736 answer, EARLY_MEDIA_SDP,
737 "Resolved answer should be the early SDP from the 183 handler"
738 );
739 assert!(
740 already_applied,
741 "remote_description_already_applied should be true when using early SDP fallback"
742 );
743 }
744
745 #[tokio::test]
749 async fn test_answer_uses_200ok_sdp_when_present() {
750 const FINAL_SDP: &str = "v=0\r\n\
751 o=- 2000 2 IN IP4 10.0.0.1\r\n\
752 s=SIP Call\r\n\
753 t=0 0\r\n\
754 m=audio 20000 RTP/AVP 0\r\n\
755 c=IN IP4 10.0.0.1\r\n\
756 a=rtpmap:0 PCMU/8000\r\n\
757 a=sendrecv\r\n";
758
759 let states = make_states(true);
760 states
761 .leg
762 .update_progress(|p| p.try_set_answer(EARLY_MEDIA_SDP));
763
764 let early = states.leg.progress.load_full().answer.clone();
765 let (answer, already_applied) = crate::call::state::resolve_final_answer(
766 Some(FINAL_SDP.as_bytes().to_vec()),
767 early.as_ref(),
768 )
769 .unwrap();
770
771 assert_eq!(
772 answer, FINAL_SDP,
773 "When 200 OK has SDP, it should be used (not the early SDP)"
774 );
775 assert!(
776 !already_applied,
777 "remote_description_already_applied should be false when 200 OK has SDP body"
778 );
779 }
780
781 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
788 async fn test_on_terminated_always_emits_events_and_records_state() {
789 use crate::event::SessionEvent;
790 use std::sync::atomic::{AtomicBool, Ordering};
791
792 let mut states = make_states(false);
793 states.terminated_reason = Some(TerminatedReason::UacCancel);
794 let leg = states.leg.clone();
795 let event_sender = states.event_sender.clone();
796 let mut event_receiver = event_sender.subscribe();
797
798 let stop = Arc::new(AtomicBool::new(false));
800 let stop2 = stop.clone();
801 let progress = leg.progress.clone();
802 let writer = crate::spawn(async move {
803 while !stop2.load(Ordering::Relaxed) {
804 progress.rcu(|p| {
807 let mut p = CallProgress::clone(p);
808 p.answer_time.get_or_insert_with(chrono::Utc::now);
809 p
810 });
811 tokio::task::yield_now().await;
812 }
813 });
814
815 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
817
818 drop(states);
820
821 stop.store(true, Ordering::Relaxed);
822 let _ = writer.await;
823
824 let mut saw_track_end = false;
826 let mut saw_hangup = false;
827 while let Ok(event) = event_receiver.try_recv() {
828 match event {
829 SessionEvent::TrackEnd { .. } => saw_track_end = true,
830 SessionEvent::Hangup { refer, .. } => {
831 assert_eq!(refer, Some(true), "refer flag comes from the leg");
832 saw_hangup = true;
833 }
834 _ => {}
835 }
836 }
837 assert!(
838 saw_track_end,
839 "TrackEnd must be emitted from on_terminated even under contention"
840 );
841 assert!(
842 saw_hangup,
843 "Hangup must be emitted from on_terminated even under contention"
844 );
845
846 let progress = leg.progress.load_full();
849 assert_eq!(progress.last_status_code, 487);
850 assert_eq!(
851 progress.hangup_reason,
852 Some(crate::callrecord::CallRecordHangupReason::Canceled)
853 );
854 }
855
856 struct CountingTrack {
859 id: TrackId,
860 config: crate::media::track::TrackConfig,
861 processor_chain: crate::media::processor::ProcessorChain,
862 updates: std::sync::Arc<std::sync::atomic::AtomicUsize>,
863 }
864
865 #[async_trait::async_trait]
866 impl crate::media::track::Track for CountingTrack {
867 fn ssrc(&self) -> u32 {
868 0
869 }
870 fn id(&self) -> &TrackId {
871 &self.id
872 }
873 fn config(&self) -> &crate::media::track::TrackConfig {
874 &self.config
875 }
876 fn processor_chain(&mut self) -> &mut crate::media::processor::ProcessorChain {
877 &mut self.processor_chain
878 }
879 async fn handshake(
880 &mut self,
881 _offer: String,
882 _timeout: Option<tokio::time::Duration>,
883 ) -> Result<String> {
884 Ok(String::new())
885 }
886 async fn update_remote_description(&mut self, _answer: &String) -> Result<()> {
887 self.updates
888 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
889 Ok(())
890 }
891 async fn update_remote_description_force(&mut self, _answer: &String) -> Result<()> {
892 self.updates
893 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
894 Ok(())
895 }
896 async fn start(
897 &mut self,
898 _event_sender: EventSender,
899 _packet_sender: crate::media::track::TrackPacketSender,
900 ) -> Result<()> {
901 Ok(())
902 }
903 async fn stop(&self) -> Result<()> {
904 Ok(())
905 }
906 async fn send_packet(&mut self, _packet: &crate::media::AudioFrame) -> Result<()> {
907 Ok(())
908 }
909 }
910
911 #[tokio::test]
917 async fn test_confirmed_applies_remote_answer_only_once() {
918 let mut states = make_states(false);
919
920 let updates = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
921 let track = CountingTrack {
922 id: states.track_id.clone(),
923 config: crate::media::track::TrackConfig::default(),
924 processor_chain: crate::media::processor::ProcessorChain::new(16000),
925 updates: updates.clone(),
926 };
927 states
928 .media_stream
929 .update_track(Box::new(track), None)
930 .await;
931
932 let endpoint = {
933 let mut builder = rsipstack::EndpointBuilder::new();
934 builder.build()
935 };
936 let dialog_layer = Arc::new(DialogLayer::new(endpoint.inner.clone()));
937
938 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<DialogState>();
939 let mut guard = DialogStateReceiverGuard::new(dialog_layer, rx, None);
940
941 let dialog_id = DialogId {
942 call_id: "test-call-id".to_string(),
943 local_tag: "test-local-tag".to_string(),
944 remote_tag: "test-remote-tag".to_string(),
945 };
946
947 let mut resp = rsipstack::rsip::Response::default();
948 resp.body = EARLY_MEDIA_SDP.as_bytes().to_vec();
949
950 tx.send(DialogState::Confirmed(dialog_id.clone(), resp.clone()))
952 .unwrap();
953 tx.send(DialogState::Confirmed(dialog_id, resp)).unwrap();
954 drop(tx);
955
956 guard
957 .dialog_event_loop(&mut states)
958 .await
959 .expect("dialog event loop must complete");
960
961 assert_eq!(
962 updates.load(std::sync::atomic::Ordering::SeqCst),
963 1,
964 "remote answer must be applied exactly once, not on the re-INVITE ACK Confirmed event"
965 );
966 }
967}