use super::Command;
use crate::{
CallOption, ReferOption,
event::{EventReceiver, EventSender, SessionEvent},
media::{
TrackId,
ambiance::AmbianceProcessor,
engine::StreamEngine,
negotiate::strip_ipv6_candidates,
processor::SubscribeProcessor,
recorder::RecorderOption,
stream::{MediaStream, MediaStreamBuilder, SERVER_SIDE_TRACK_ID},
track::{
Track, TrackConfig,
file::FileTrack,
forwarding::ForwardingTrack,
media_pass::MediaPassTrack,
rtc::{RtcTrack, RtcTrackConfig},
tts::SynthesisHandle,
websocket::{WebsocketBytesReceiver, WebsocketTrack},
},
},
synthesis::{SynthesisCommand, SynthesisOption},
transcription::TranscriptionOption,
};
use crate::{
app::AppState,
call::{
CommandReceiver, CommandSender,
sip::{DialogStateReceiverGuard, Invitation, InviteDialogStates},
},
callrecord::{CallRecord, CallRecordEvent, CallRecordEventType, CallRecordHangupReason},
useragent::{
invitation::PendingDialog,
public_address::{
build_public_contact_uri, contact_needs_public_resolution, find_local_addr_for_uri,
},
},
};
use anyhow::Result;
use audio_codec::CodecType;
use chrono::{DateTime, Utc};
use rsipstack::dialog::{invitation::InviteOption, invite_dialog::InviteDialog};
use rsipstack::rsip::prelude::HeadersExt;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
path::Path,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use tokio::{fs::File, select, sync::Mutex, sync::RwLock, sync::mpsc, time::sleep};
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn};
pub enum PendingCallerTrack {
StartedForEarlyMedia,
NotStarted(Box<dyn Track>),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::AppStateBuilder;
use crate::callrecord::CallRecordHangupReason;
use crate::config::Config;
use crate::media::track::tts::SynthesisHandle;
use crate::synthesis::SynthesisCommand;
use tokio::sync::mpsc;
#[tokio::test]
async fn test_tts_ssrc_reuse_for_autohangup() -> Result<()> {
let mut config = Config::default();
config.udp_port = 0; config.media_cache_path = "/tmp/mediacache".to_string();
let stream_engine = Arc::new(StreamEngine::default());
let app_state = AppStateBuilder::new()
.with_config(config)
.with_stream_engine(stream_engine)
.build()
.await?;
let cancel_token = CancellationToken::new();
let session_id = "test-session".to_string();
let track_config = TrackConfig::default();
let mut option = crate::CallOption::default();
option.tts = Some(crate::synthesis::SynthesisOption::default());
let active_call = Arc::new(ActiveCall::new(
ActiveCallType::Sip,
cancel_token.clone(),
session_id.clone(),
app_state.invitation.clone(),
app_state.clone(),
track_config,
None,
false,
None,
None,
None,
));
{
let mut state = active_call.call_state.write().await;
state.option = Some(option);
}
let (tx, mut rx) = mpsc::unbounded_channel::<SynthesisCommand>();
let initial_ssrc = 12345;
let handle = SynthesisHandle::new(tx, Some("play_1".to_string()), initial_ssrc);
{
let mut state = active_call.call_state.write().await;
state.tts_handle = Some(handle);
state.current_play_id = Some("play_1".to_string());
}
active_call
.do_tts(
"hangup now".to_string(),
None,
Some("play_1".to_string()),
Some(true),
false,
true,
None,
None,
false,
None,
)
.await?;
{
let state = active_call.call_state.read().await;
assert!(state.auto_hangup.is_some());
let (h_ssrc, reason) = state.auto_hangup.clone().unwrap();
assert_eq!(
h_ssrc, initial_ssrc,
"SSRC should be reused from existing handle"
);
assert_eq!(reason, CallRecordHangupReason::BySystem);
}
let cmd = rx.try_recv().expect("Should have received tts command");
assert_eq!(cmd.text, "hangup now");
Ok(())
}
#[tokio::test]
async fn test_tts_new_ssrc_for_different_play_id() -> Result<()> {
let mut config = Config::default();
config.udp_port = 0; config.media_cache_path = "/tmp/mediacache".to_string();
let stream_engine = Arc::new(StreamEngine::default());
let app_state = AppStateBuilder::new()
.with_config(config)
.with_stream_engine(stream_engine)
.build()
.await?;
let active_call = Arc::new(ActiveCall::new(
ActiveCallType::Sip,
CancellationToken::new(),
"test-session".to_string(),
app_state.invitation.clone(),
app_state.clone(),
TrackConfig::default(),
None,
false,
None,
None,
None,
));
let mut tts_opt = crate::synthesis::SynthesisOption::default();
tts_opt.provider = Some(crate::synthesis::SynthesisType::Aliyun);
let mut option = crate::CallOption::default();
option.tts = Some(tts_opt);
{
let mut state = active_call.call_state.write().await;
state.option = Some(option);
}
let (tx, _rx) = mpsc::unbounded_channel();
let initial_ssrc = 111;
let handle = SynthesisHandle::new(tx, Some("play_1".to_string()), initial_ssrc);
{
let mut state = active_call.call_state.write().await;
state.tts_handle = Some(handle);
state.current_play_id = Some("play_1".to_string());
}
active_call
.do_tts(
"new play".to_string(),
None,
Some("play_2".to_string()),
Some(true),
false,
true,
None,
None,
false,
None,
)
.await?;
{
let state = active_call.call_state.read().await;
let (h_ssrc, _) = state.auto_hangup.clone().unwrap();
assert_ne!(
h_ssrc, initial_ssrc,
"Should use a new SSRC for different play_id"
);
}
Ok(())
}
async fn make_active_call() -> Arc<ActiveCall> {
let mut config = Config::default();
config.udp_port = 0;
config.media_cache_path = "/tmp/mediacache".to_string();
let app_state = AppStateBuilder::new()
.with_config(config)
.with_stream_engine(Arc::new(StreamEngine::default()))
.build()
.await
.unwrap();
Arc::new(ActiveCall::new(
ActiveCallType::Sip,
CancellationToken::new(),
"test-session".to_string(),
app_state.invitation.clone(),
app_state.clone(),
TrackConfig::default(),
None,
false,
None,
None,
None,
))
}
#[tokio::test]
async fn test_hangup_refer_true_cancels_refer_only() -> Result<()> {
let active_call = make_active_call().await;
let refer_token = active_call.cancel_token.child_token();
let refer_state = Arc::new(RwLock::new(ActiveCallState {
ssrc: 1,
is_refer: true,
..Default::default()
}));
{
let mut cs = active_call.call_state.write().await;
cs.refer_call_token = Some(refer_token.clone());
cs.refer_callstate = Some(refer_state.clone());
}
active_call.do_hangup(None, None, None, Some(true)).await?;
assert!(
refer_token.is_cancelled(),
"refer token should be cancelled"
);
assert!(
!active_call.media_stream.cancel_token.is_cancelled(),
"media stream should NOT stop"
);
assert!(
refer_state.read().await.hangup_reason.is_some(),
"hangup_reason should be set on refer state"
);
Ok(())
}
#[tokio::test]
async fn test_hangup_none_cancels_refer_too() -> Result<()> {
let active_call = make_active_call().await;
let refer_token = active_call.cancel_token.child_token();
{
let mut cs = active_call.call_state.write().await;
cs.refer_call_token = Some(refer_token.clone());
}
active_call.do_hangup(None, None, None, None).await?;
assert!(
refer_token.is_cancelled(),
"refer token should be cancelled"
);
assert!(
active_call.media_stream.cancel_token.is_cancelled(),
"media stream should stop"
);
Ok(())
}
struct MockCallerTrack {
id: TrackId,
config: crate::media::track::TrackConfig,
processor_chain: crate::media::processor::ProcessorChain,
}
impl MockCallerTrack {
fn new(id: TrackId) -> Self {
Self {
id,
config: crate::media::track::TrackConfig::default(),
processor_chain: crate::media::processor::ProcessorChain::new(16000),
}
}
}
#[async_trait::async_trait]
impl crate::media::track::Track for MockCallerTrack {
fn ssrc(&self) -> u32 {
0
}
fn id(&self) -> &TrackId {
&self.id
}
fn config(&self) -> &crate::media::track::TrackConfig {
&self.config
}
fn processor_chain(&mut self) -> &mut crate::media::processor::ProcessorChain {
&mut self.processor_chain
}
async fn handshake(
&mut self,
_o: String,
_t: Option<tokio::time::Duration>,
) -> Result<String> {
Ok(String::new())
}
async fn update_remote_description(&mut self, _a: &String) -> Result<()> {
Ok(())
}
async fn start(
&mut self,
_e: crate::event::EventSender,
_p: crate::media::track::TrackPacketSender,
) -> Result<()> {
Ok(())
}
async fn stop(&self) -> Result<()> {
Ok(())
}
async fn send_packet(&mut self, _f: &crate::media::AudioFrame) -> Result<()> {
Ok(())
}
}
struct MockAsrClient;
#[async_trait::async_trait]
impl crate::transcription::TranscriptionClient for MockAsrClient {
fn send_audio(
&self,
_s: &[crate::media::Sample],
_src: Option<&crate::media::SourcePacket>,
) -> Result<()> {
Ok(())
}
}
#[tokio::test]
async fn test_setup_track_with_stream_builds_processors_from_accept_option() -> Result<()> {
let (asr_created_tx, mut asr_created_rx) = mpsc::channel::<()>(1);
let mock_provider =
crate::transcription::TranscriptionType::Other("mock-ringing-asr".to_string());
let mut engine = StreamEngine::new();
engine.register_asr(
mock_provider.clone(),
Box::new(move |_tid, _tok, _opt, _es| {
let tx = asr_created_tx.clone();
Box::pin(async move {
let _ = tx.send(()).await;
Ok(Box::new(MockAsrClient)
as Box<dyn crate::transcription::TranscriptionClient>)
})
}),
);
let engine = Arc::new(engine);
let mut config = Config::default();
config.udp_port = 0;
config.media_cache_path = "/tmp/mediacache_ringing_accept_test".to_string();
let app_state = AppStateBuilder::new()
.with_config(config)
.with_stream_engine(engine)
.build()
.await?;
let cancel_token = CancellationToken::new();
let session_id = format!("test-ringing-accept-{}", uuid::Uuid::new_v4());
let active_call = Arc::new(ActiveCall::new(
ActiveCallType::Sip,
cancel_token.clone(),
session_id.clone(),
app_state.invitation.clone(),
app_state.clone(),
TrackConfig::default(),
None,
false,
None,
None,
None,
));
let mock_track = Box::new(MockCallerTrack::new(session_id.clone()));
let accept_option = crate::CallOption {
asr: Some(crate::transcription::TranscriptionOption {
provider: Some(mock_provider),
..Default::default()
}),
..Default::default()
};
active_call
.setup_track_with_stream(&accept_option, mock_track)
.await?;
let received =
tokio::time::timeout(std::time::Duration::from_secs(3), asr_created_rx.recv()).await;
assert!(
received.is_ok() && received.unwrap().is_some(),
"ASR processor was NOT created — setup_track_with_stream did not build \
processors from the accept option (regression: ringing-before-accept)"
);
cancel_token.cancel();
Ok(())
}
#[tokio::test]
async fn test_update_track_wrapper_does_not_build_asr_processor() -> Result<()> {
let (asr_created_tx, mut asr_created_rx) = mpsc::channel::<()>(1);
let mock_provider =
crate::transcription::TranscriptionType::Other("mock-ringing-asr".to_string());
let mut engine = StreamEngine::new();
engine.register_asr(
mock_provider.clone(),
Box::new(move |_tid, _tok, _opt, _es| {
let tx = asr_created_tx.clone();
Box::pin(async move {
let _ = tx.send(()).await;
Ok(Box::new(MockAsrClient)
as Box<dyn crate::transcription::TranscriptionClient>)
})
}),
);
let engine = Arc::new(engine);
let mut config = Config::default();
config.udp_port = 0;
config.media_cache_path = "/tmp/mediacache_update_track_wrapper_test".to_string();
let app_state = AppStateBuilder::new()
.with_config(config)
.with_stream_engine(engine)
.build()
.await?;
let cancel_token = CancellationToken::new();
let session_id = format!("test-no-asr-on-prepare-{}", uuid::Uuid::new_v4());
let active_call = Arc::new(ActiveCall::new(
ActiveCallType::Sip,
cancel_token.clone(),
session_id.clone(),
app_state.invitation.clone(),
app_state.clone(),
TrackConfig::default(),
None,
false,
None,
None,
None,
));
{
let mut cs = active_call.call_state.write().await;
cs.option = Some(crate::CallOption {
asr: Some(crate::transcription::TranscriptionOption {
provider: Some(mock_provider),
..Default::default()
}),
..Default::default()
});
}
let mock_track = Box::new(MockCallerTrack::new(session_id.clone()));
active_call.update_track_wrapper(mock_track, None).await;
let received =
tokio::time::timeout(std::time::Duration::from_millis(500), asr_created_rx.recv())
.await;
assert!(
received.is_err(),
"ASR builder fired during track preparation — double-ASR regression"
);
cancel_token.cancel();
Ok(())
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallParams {
pub id: Option<String>,
#[serde(rename = "dump")]
pub dump_events: Option<bool>,
#[serde(rename = "ping")]
pub ping_interval: Option<u32>,
pub server_side_track: Option<String>,
#[serde(default)]
pub forward: Option<bool>,
#[serde(default)]
pub visited: Option<String>,
}
impl CallParams {
pub fn to_forward_query(&self, visited: &str) -> String {
let mut parts: Vec<String> = Vec::new();
if let Some(id) = &self.id {
parts.push(format!("id={}", urlencoding::encode(id)));
}
if let Some(dump) = self.dump_events {
parts.push(format!("dump={}", dump));
}
if let Some(ping) = self.ping_interval {
parts.push(format!("ping={}", ping));
}
if let Some(track) = &self.server_side_track {
parts.push(format!(
"server_side_track={}",
urlencoding::encode(track)
));
}
parts.push("forward=true".to_string());
parts.push(format!("visited={}", urlencoding::encode(visited)));
parts.join("&")
}
pub fn visited_list(&self) -> Vec<String> {
self.visited
.as_deref()
.map(|v| {
v.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default()
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ActiveCallType {
Webrtc,
B2bua,
WebSocket,
#[default]
Sip,
}
#[derive(Default)]
pub struct ActiveCallState {
pub session_id: String,
pub start_time: DateTime<Utc>,
pub ring_time: Option<DateTime<Utc>>,
pub answer_time: Option<DateTime<Utc>>,
pub hangup_reason: Option<CallRecordHangupReason>,
pub last_status_code: u16,
pub option: Option<CallOption>,
pub answer: Option<String>,
pub ssrc: u32,
pub refer_callstate: Option<ActiveCallStateRef>,
pub extras: Option<HashMap<String, serde_json::Value>>,
pub is_refer: bool,
pub sip_hangup_headers_template: Option<HashMap<String, String>>,
pub tts_handle: Option<SynthesisHandle>,
pub auto_hangup: Option<(u32, CallRecordHangupReason)>,
pub wait_input_timeout: Option<u32>,
pub moh: Option<String>,
pub current_play_id: Option<String>,
pub audio_receiver: Option<WebsocketBytesReceiver>,
pub ready_to_answer: Option<(String, PendingCallerTrack, InviteDialog)>,
pub pending_asr_resume: Option<(u32, TranscriptionOption)>,
pub bridge_paused: Arc<AtomicBool>,
pub refer_call_token: Option<CancellationToken>,
}
pub type ActiveCallRef = Arc<ActiveCall>;
pub type ActiveCallStateRef = Arc<RwLock<ActiveCallState>>;
pub struct ActiveCall {
pub call_state: ActiveCallStateRef,
pub cancel_token: CancellationToken,
pub call_type: ActiveCallType,
pub session_id: String,
pub media_stream: Arc<MediaStream>,
pub track_config: TrackConfig,
pub event_sender: EventSender,
pub app_state: AppState,
pub invitation: Invitation,
pub cmd_sender: CommandSender,
pub dump_events: bool,
pub server_side_track_id: TrackId,
}
pub struct ActiveCallGuard {
pub call: ActiveCallRef,
pub active_calls: usize,
}
impl ActiveCallGuard {
pub fn new(call: ActiveCallRef) -> Self {
let active_calls = {
call.app_state
.total_calls
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let mut calls = call.app_state.active_calls.lock().unwrap();
calls.insert(call.session_id.clone(), call.clone());
calls.len()
};
Self { call, active_calls }
}
}
impl Drop for ActiveCallGuard {
fn drop(&mut self) {
self.call
.app_state
.active_calls
.lock()
.unwrap()
.remove(&self.call.session_id);
}
}
pub struct ActiveCallReceiver {
pub cmd_receiver: CommandReceiver,
pub dump_cmd_receiver: CommandReceiver,
pub dump_event_receiver: EventReceiver,
}
impl ActiveCall {
pub fn new(
call_type: ActiveCallType,
cancel_token: CancellationToken,
session_id: String,
invitation: Invitation,
app_state: AppState,
track_config: TrackConfig,
audio_receiver: Option<WebsocketBytesReceiver>,
dump_events: bool,
server_side_track_id: Option<TrackId>,
extras: Option<HashMap<String, serde_json::Value>>,
sip_hangup_headers_template: Option<HashMap<String, String>>,
) -> Self {
let event_sender = crate::event::create_event_sender();
let cmd_sender = tokio::sync::broadcast::Sender::<Command>::new(32);
let server_side_track_id = server_side_track_id.unwrap_or(SERVER_SIDE_TRACK_ID.to_string());
let media_stream_builder = MediaStreamBuilder::new(event_sender.clone())
.with_id(session_id.clone())
.with_cancel_token(cancel_token.child_token());
let media_stream = Arc::new(media_stream_builder.build());
let start_time = Utc::now();
let call_type_str = match &call_type {
ActiveCallType::Sip => "sip",
ActiveCallType::WebSocket => "websocket",
ActiveCallType::Webrtc => "webrtc",
ActiveCallType::B2bua => "b2bua",
};
let extras = {
let mut e = extras.unwrap_or_default();
e.entry(crate::playbook::BUILTIN_SESSION_ID.to_string())
.or_insert_with(|| serde_json::Value::String(session_id.clone()));
e.entry(crate::playbook::BUILTIN_CALL_TYPE.to_string())
.or_insert_with(|| serde_json::Value::String(call_type_str.to_string()));
e.entry(crate::playbook::BUILTIN_START_TIME.to_string())
.or_insert_with(|| serde_json::Value::String(start_time.to_rfc3339()));
Some(e)
};
let call_state = Arc::new(RwLock::new(ActiveCallState {
session_id: session_id.clone(),
start_time,
ssrc: rand::random::<u32>(),
extras,
audio_receiver,
sip_hangup_headers_template,
..Default::default()
}));
Self {
cancel_token,
call_type,
session_id,
call_state,
media_stream,
track_config,
event_sender,
app_state,
invitation,
cmd_sender,
dump_events,
server_side_track_id,
}
}
pub async fn enqueue_command(&self, command: Command) -> Result<()> {
self.cmd_sender
.send(command)
.map_err(|e| anyhow::anyhow!("Failed to send command: {}", e))?;
Ok(())
}
pub fn new_receiver(&self) -> ActiveCallReceiver {
ActiveCallReceiver {
cmd_receiver: self.cmd_sender.subscribe(),
dump_cmd_receiver: self.cmd_sender.subscribe(),
dump_event_receiver: self.event_sender.subscribe(),
}
}
pub async fn serve(&self, receiver: ActiveCallReceiver) -> Result<()> {
let ActiveCallReceiver {
mut cmd_receiver,
dump_cmd_receiver,
dump_event_receiver,
} = receiver;
let process_command_loop = async move {
while let Ok(command) = cmd_receiver.recv().await {
match Box::pin(self.dispatch(command)).await {
Ok(_) => (),
Err(e) => {
warn!(session_id = self.session_id, "{}", e);
self.event_sender
.send(SessionEvent::Error {
track_id: self.session_id.clone(),
timestamp: crate::media::get_timestamp(),
sender: "command".to_string(),
error: e.to_string(),
code: None,
})
.ok();
}
}
}
};
self.app_state
.total_calls
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
tokio::join!(
self.dump_loop(self.dump_events, dump_cmd_receiver, dump_event_receiver),
async {
select! {
_ = process_command_loop => {
info!(session_id = self.session_id, "command loop done");
}
_ = self.process() => {
info!(session_id = self.session_id, "call serve done");
}
_ = self.cancel_token.cancelled() => {
info!(session_id = self.session_id, "call cancelled - cleaning up resources");
}
}
self.cancel_token.cancel();
}
);
Ok(())
}
async fn process(&self) -> Result<()> {
let mut event_receiver = self.event_sender.subscribe();
let input_timeout_expire = Arc::new(Mutex::new((0u64, 0u32)));
let input_timeout_expire_ref = input_timeout_expire.clone();
let event_sender = self.event_sender.clone();
let wait_input_timeout_loop = async {
loop {
let (start_time, expire) = { *input_timeout_expire.lock().await };
if expire > 0 && crate::media::get_timestamp() >= start_time + expire as u64 {
info!(session_id = self.session_id, "wait input timeout reached");
*input_timeout_expire.lock().await = (0, 0);
let is_refer = self.call_state.read().await.is_refer;
event_sender
.send(SessionEvent::Silence {
track_id: self.server_side_track_id.clone(),
timestamp: crate::media::get_timestamp(),
start_time,
duration: expire as u64,
samples: None,
refer: Some(is_refer),
})
.ok();
}
sleep(Duration::from_millis(100)).await;
}
};
let server_side_track_id = self.server_side_track_id.clone();
let event_hook_loop = async move {
while let Ok(event) = event_receiver.recv().await {
match event {
SessionEvent::Speaking { .. }
| SessionEvent::Dtmf { .. }
| SessionEvent::AsrDelta { .. }
| SessionEvent::AsrFinal { .. }
| SessionEvent::TrackStart { .. } => {
*input_timeout_expire_ref.lock().await = (0, 0);
}
SessionEvent::TrackEnd {
track_id,
play_id,
ssrc,
..
} => {
if track_id != server_side_track_id {
continue;
}
let (moh_path, auto_hangup, wait_timeout_val) = {
let mut state = self.call_state.write().await;
if play_id != state.current_play_id {
debug!(
session_id = self.session_id,
?play_id,
current = ?state.current_play_id,
"ignoring interrupted track end"
);
continue;
}
state.current_play_id = None;
(
state.moh.clone(),
state.auto_hangup.clone(),
state.wait_input_timeout.take(),
)
};
if let Some(path) = moh_path {
info!(session_id = self.session_id, "looping moh: {}", path);
let ssrc = rand::random::<u32>();
let file_track = FileTrack::new(self.server_side_track_id.clone())
.with_play_id(Some(path.clone()))
.with_ssrc(ssrc)
.with_path(path.clone())
.with_cancel_token(self.cancel_token.child_token());
self.update_track_wrapper(Box::new(file_track), Some(path))
.await;
continue;
}
if let Some((hangup_ssrc, hangup_reason)) = auto_hangup {
if hangup_ssrc == ssrc {
info!(
session_id = self.session_id,
ssrc, "auto hangup when track end track_id:{}", track_id
);
self.do_hangup(Some(hangup_reason), None, None, None)
.await
.ok();
}
}
if let Some(timeout) = wait_timeout_val {
let expire = if timeout > 0 {
(crate::media::get_timestamp(), timeout)
} else {
(0, 0)
};
*input_timeout_expire_ref.lock().await = expire;
}
}
SessionEvent::Interrupt { receiver } => {
let track_id =
receiver.unwrap_or_else(|| self.server_side_track_id.clone());
if track_id == self.server_side_track_id {
debug!(
session_id = self.session_id,
"received interrupt event, stopping playback"
);
self.do_interrupt(true).await.ok();
}
}
SessionEvent::Inactivity { track_id, .. } => {
info!(
session_id = self.session_id,
track_id, "inactivity timeout reached, hanging up"
);
self.do_hangup(
Some(CallRecordHangupReason::InactivityTimeout),
None,
None,
None,
)
.await
.ok();
}
SessionEvent::Hangup { refer, .. } => {
if refer == Some(true) {
let mut cs = self.call_state.write().await;
if let Some((refer_ssrc, asr_option)) = cs.pending_asr_resume.take() {
let is_refer_hangup = cs
.refer_callstate
.as_ref()
.map(|rcs| {
rcs.try_read()
.map(|g| g.ssrc == refer_ssrc)
.unwrap_or(false)
})
.unwrap_or(false);
if is_refer_hangup {
drop(cs); info!(
session_id = self.session_id,
"Refer call ended, resuming parent ASR"
);
match self
.app_state
.stream_engine
.create_asr_processor(
self.server_side_track_id.clone(),
self.cancel_token.child_token(),
asr_option,
self.event_sender.clone(),
)
.await
{
Ok(asr_processor) => {
if let Err(e) = self
.media_stream
.append_processor(
&self.server_side_track_id,
asr_processor,
)
.await
{
warn!(
session_id = self.session_id,
"Failed to resume ASR after refer: {}", e
);
}
}
Err(e) => {
warn!(
session_id = self.session_id,
"Failed to create ASR processor for resume: {}", e
);
}
}
}
}
}
}
SessionEvent::Error { track_id, .. } => {
if track_id != server_side_track_id {
continue;
}
let moh_info = {
let mut state = self.call_state.write().await;
if let Some(path) = state.moh.clone() {
let fallback = "./config/sounds/refer_moh.wav".to_string();
let next_path = if path != fallback
&& std::path::Path::new(&fallback).exists()
{
info!(
session_id = self.session_id,
"moh error, switching to fallback: {}", fallback
);
state.moh = Some(fallback.clone());
fallback
} else {
info!(
session_id = self.session_id,
"looping moh on error: {}", path
);
path
};
Some(next_path)
} else {
None
}
};
if let Some(next_path) = moh_info {
let ssrc = rand::random::<u32>();
let file_track = FileTrack::new(self.server_side_track_id.clone())
.with_play_id(Some(next_path.clone()))
.with_ssrc(ssrc)
.with_path(next_path.clone())
.with_cancel_token(self.cancel_token.child_token());
self.update_track_wrapper(Box::new(file_track), Some(next_path))
.await;
continue;
}
}
SessionEvent::Hold { on_hold, .. } => {
self.call_state
.read()
.await
.bridge_paused
.store(on_hold, Ordering::Relaxed);
}
_ => {}
}
}
};
select! {
_ = wait_input_timeout_loop=>{
info!(session_id = self.session_id, "wait input timeout loop done");
}
_ = self.media_stream.serve() => {
info!(session_id = self.session_id, "media stream loop done");
}
_ = event_hook_loop => {
info!(session_id = self.session_id, "event loop done");
}
}
Ok(())
}
async fn dispatch(&self, command: Command) -> Result<()> {
match command {
Command::Invite { option } => self.do_invite(option).await,
Command::Accept { option } => self.do_accept(option).await,
Command::Reject { reason, code } => {
self.do_reject(code.map(|c| (c as u16).into()), Some(reason))
.await
}
Command::Ringing {
ringtone,
recorder,
early_media,
} => self.do_ringing(ringtone, recorder, early_media).await,
Command::Tts {
text,
speaker,
play_id,
auto_hangup,
streaming,
end_of_stream,
option,
wait_input_timeout,
base64,
cache_key,
} => {
self.do_tts(
text,
speaker,
play_id,
auto_hangup,
streaming.unwrap_or_default(),
end_of_stream.unwrap_or_default(),
option,
wait_input_timeout,
base64.unwrap_or_default(),
cache_key,
)
.await
}
Command::Play {
url,
play_id,
auto_hangup,
wait_input_timeout,
offset_ms,
} => {
self.do_play(url, play_id, auto_hangup, wait_input_timeout, offset_ms)
.await
}
Command::Hangup {
reason,
initiator,
headers,
refer,
} => {
let reason = reason.map(|r| {
r.parse::<CallRecordHangupReason>()
.unwrap_or(CallRecordHangupReason::BySystem)
});
self.do_hangup(reason, initiator, headers, refer).await
}
Command::Refer {
caller,
callee,
options,
} => self.do_refer(caller, callee, options).await,
Command::Message {
body,
content_type,
headers,
refer,
} => self.do_message(body, content_type, headers, refer).await,
Command::Bridge { target_session_id } => self.do_bridge(target_session_id).await,
Command::Unbridge { target_session_id } => self.do_unbridge(target_session_id).await,
Command::Mute { track_id } => self.do_mute(track_id).await,
Command::Unmute { track_id } => self.do_unmute(track_id).await,
Command::Pause {} => self.do_pause().await,
Command::Resume {} => self.do_resume().await,
Command::Interrupt {
graceful: passage,
fade_out_ms: _,
} => self.do_interrupt(passage.unwrap_or_default()).await,
Command::History { speaker, text } => self.do_history(speaker, text).await,
Command::Custom { sender, data } => self.do_custom(sender, data),
}
}
fn build_record_option(&self, option: &CallOption) -> Option<RecorderOption> {
if let Some(recorder_option) = &option.recorder {
let mut recorder_file = recorder_option.recorder_file.clone();
if recorder_file.contains("{id}") {
recorder_file = recorder_file.replace("{id}", &self.session_id);
}
let recorder_file = if recorder_file.is_empty() {
self.app_state.get_recorder_file(&self.session_id)
} else {
let p = Path::new(&recorder_file);
p.is_absolute()
.then(|| recorder_file.clone())
.unwrap_or_else(|| self.app_state.get_recorder_file(&recorder_file))
};
info!(
session_id = self.session_id,
recorder_file, "created recording file"
);
let track_samplerate = self.track_config.samplerate;
let recorder_samplerate = if track_samplerate > 0 {
track_samplerate
} else {
recorder_option.samplerate
};
let recorder_ptime = if recorder_option.ptime == 0 {
200
} else {
recorder_option.ptime
};
let requested_format = recorder_option
.format
.unwrap_or(self.app_state.config.recorder_format());
let format = requested_format.effective();
if requested_format != format {
warn!(
session_id = self.session_id,
requested = requested_format.extension(),
"Recorder format fallback to wav due to unsupported feature"
);
}
let mut recorder_config = RecorderOption {
recorder_file,
samplerate: recorder_samplerate,
ptime: recorder_ptime,
format: Some(format),
};
recorder_config.ensure_path_extension(format);
Some(recorder_config)
} else {
None
}
}
async fn invite_or_accept(&self, mut option: CallOption, sender: String) -> Result<CallOption> {
{
let state = self.call_state.read().await;
option = state.merge_option(option);
}
option.check_default();
if let Some(opt) = self.build_record_option(&option) {
self.media_stream.update_recorder_option(opt).await;
}
if let Some(opt) = &option.media_pass {
let track_id = self.server_side_track_id.clone();
let cancel_token = self.cancel_token.child_token();
let ssrc = rand::random::<u32>();
let media_pass_track = MediaPassTrack::new(
self.session_id.clone(),
ssrc,
track_id,
cancel_token,
opt.clone(),
);
self.update_track_wrapper(Box::new(media_pass_track), None)
.await;
}
info!(
session_id = self.session_id,
call_type = ?self.call_type,
sender,
?option,
"caller with option"
);
match self.setup_caller_track(&option).await {
Ok(_) => return Ok(option),
Err(e) => {
self.app_state
.total_failed_calls
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let error_event = crate::event::SessionEvent::Error {
track_id: self.session_id.clone(),
timestamp: crate::media::get_timestamp(),
sender,
error: e.to_string(),
code: None,
};
self.event_sender.send(error_event).ok();
self.do_hangup(Some(CallRecordHangupReason::BySystem), None, None, None)
.await
.ok();
return Err(e);
}
}
}
async fn do_invite(&self, option: CallOption) -> Result<()> {
self.invite_or_accept(option, "invite".to_string())
.await
.map(|_| ())
}
async fn do_accept(&self, mut option: CallOption) -> Result<()> {
let has_pending = self
.invitation
.find_dialog_id_by_session_id(&self.session_id)
.is_some();
let ready_to_answer_val = {
let state = self.call_state.read().await;
state.ready_to_answer.is_none()
};
if ready_to_answer_val {
if !has_pending {
warn!(session_id = self.session_id, "no pending call to accept");
let rejet_event = crate::event::SessionEvent::Reject {
track_id: self.session_id.clone(),
timestamp: crate::media::get_timestamp(),
reason: "no pending call".to_string(),
refer: None,
code: Some(486),
};
self.event_sender.send(rejet_event).ok();
self.do_hangup(Some(CallRecordHangupReason::BySystem), None, None, None)
.await
.ok();
return Err(anyhow::anyhow!("no pending call to accept"));
}
option = self.invite_or_accept(option, "accept".to_string()).await?;
} else {
option.check_default();
if let Some(opt) = self.build_record_option(&option) {
self.media_stream.update_recorder_option(opt).await;
}
self.call_state.write().await.option = Some(option.clone());
}
info!(session_id = self.session_id, ?option, "accepting call");
let ready = self.call_state.write().await.ready_to_answer.take();
if let Some((answer, pending_track, dialog)) = ready {
info!(session_id = self.session_id, "ready to answer with track");
let headers = vec![rsipstack::rsip::Header::ContentType(
"application/sdp".to_string().into(),
)];
match dialog.accept(Some(headers), Some(answer.as_bytes().to_vec())) {
Ok(_) => {
{
let mut state = self.call_state.write().await;
state.answer = Some(answer);
state.answer_time = Some(Utc::now());
}
self.finish_caller_stack(&option, pending_track).await?;
}
Err(e) => {
warn!(session_id = self.session_id, "failed to accept call: {}", e);
return Err(anyhow::anyhow!("failed to accept call"));
}
}
}
return Ok(());
}
async fn do_reject(
&self,
code: Option<rsipstack::rsip::StatusCode>,
reason: Option<String>,
) -> Result<()> {
match self
.invitation
.find_dialog_id_by_session_id(&self.session_id)
{
Some(id) => {
info!(
session_id = self.session_id,
?reason,
?code,
"rejecting call"
);
let result = self.invitation.hangup(id, code, reason).await;
if result.is_ok() {
self.cancel_token.cancel();
}
result
}
None => {
let ready = self.call_state.write().await.ready_to_answer.take();
if let Some((_, _, dialog)) = ready {
info!(
session_id = self.session_id,
?reason,
?code,
"rejecting call from ready_to_answer"
);
let dialog_id = dialog.id();
dialog.reject(code, reason).ok();
self.invitation.dialog_layer.remove_dialog(&dialog_id);
self.cancel_token.cancel();
}
Ok(())
}
}
}
async fn do_ringing(
&self,
ringtone: Option<String>,
recorder: Option<RecorderOption>,
early_media: Option<bool>,
) -> Result<()> {
let ready_to_answer_val = self.call_state.read().await.ready_to_answer.is_none();
if ready_to_answer_val {
let option = CallOption {
recorder,
..Default::default()
};
let _ = self.invite_or_accept(option, "ringing".to_string()).await?;
}
let state = self.call_state.read().await;
if let Some((answer, _, dialog)) = state.ready_to_answer.as_ref() {
let (headers, body) = if early_media.unwrap_or_default() || ringtone.is_some() {
let headers = vec![rsipstack::rsip::Header::ContentType(
"application/sdp".to_string().into(),
)];
(Some(headers), Some(answer.as_bytes().to_vec()))
} else {
(None, None)
};
dialog.ringing(headers, body).ok();
info!(
session_id = self.session_id,
ringtone, early_media, "playing ringtone"
);
if let Some(ringtone_url) = ringtone {
drop(state);
self.do_play(ringtone_url, None, None, None, None)
.await
.ok();
} else {
info!(session_id = self.session_id, "no ringtone to play");
}
}
Ok(())
}
async fn do_tts(
&self,
text: String,
speaker: Option<String>,
play_id: Option<String>,
auto_hangup: Option<bool>,
streaming: bool,
end_of_stream: bool,
option: Option<SynthesisOption>,
wait_input_timeout: Option<u32>,
base64: bool,
cache_key: Option<String>,
) -> Result<()> {
let tts_option = {
let call_state = self.call_state.read().await;
match call_state.option.clone().unwrap_or_default().tts {
Some(opt) => opt.merge_with(option),
None => {
if let Some(opt) = option {
opt
} else {
return Err(anyhow::anyhow!("no tts option available"));
}
}
}
};
let speaker = match speaker {
Some(s) => Some(s),
None => tts_option.speaker.clone(),
};
let mut play_command = SynthesisCommand {
text,
speaker,
play_id: play_id.clone(),
streaming,
end_of_stream: if !streaming { true } else { end_of_stream },
option: tts_option,
base64,
cache_key,
};
info!(
session_id = self.session_id,
provider = ?play_command.option.provider,
text = %play_command.text.chars().take(10).collect::<String>(),
speaker = play_command.speaker.as_deref(),
auto_hangup = auto_hangup.unwrap_or_default(),
play_id = play_command.play_id.as_deref(),
streaming = play_command.streaming,
end_of_stream = play_command.end_of_stream,
wait_input_timeout = wait_input_timeout.unwrap_or_default(),
is_base64 = play_command.base64,
cache_key = play_command.cache_key.as_deref(),
"new synthesis"
);
let ssrc = rand::random::<u32>();
let (should_interrupt, picked_ssrc) = {
let mut state = self.call_state.write().await;
let (target_ssrc, changed) = if let Some(handle) = &state.tts_handle {
if play_id.is_some() && state.current_play_id != play_id {
(ssrc, true)
} else {
(handle.ssrc, false)
}
} else {
(ssrc, false)
};
state.wait_input_timeout = wait_input_timeout;
state.current_play_id = play_id.clone();
(changed, target_ssrc)
};
if should_interrupt {
let _ = self.do_interrupt(false).await;
}
{
let mut state = self.call_state.write().await;
state.auto_hangup = match auto_hangup {
Some(true) => Some((picked_ssrc, CallRecordHangupReason::BySystem)),
_ => {
if state.tts_handle.is_some() && !should_interrupt {
state.auto_hangup.clone()
} else {
None
}
}
};
}
let existing_handle = self.call_state.read().await.tts_handle.clone();
if let Some(tts_handle) = existing_handle {
match tts_handle.try_send(play_command) {
Ok(_) => return Ok(()),
Err(e) => {
play_command = e.0;
}
}
}
let (new_handle, tts_track) = StreamEngine::create_tts_track(
self.app_state.stream_engine.clone(),
self.cancel_token.child_token(),
self.session_id.clone(),
self.server_side_track_id.clone(),
picked_ssrc,
play_id.clone(),
streaming,
&play_command.option,
)
.await?;
new_handle.try_send(play_command)?;
self.call_state.write().await.tts_handle = Some(new_handle);
self.update_track_wrapper(tts_track, play_id).await;
Ok(())
}
async fn do_play(
&self,
url: String,
play_id: Option<String>,
auto_hangup: Option<bool>,
wait_input_timeout: Option<u32>,
offset_ms: Option<u32>,
) -> Result<()> {
let ssrc = rand::random::<u32>();
info!(
session_id = self.session_id,
ssrc, url, play_id, auto_hangup, "play file track"
);
let play_id = play_id.or(Some(url.clone()));
let mut file_track = FileTrack::new(self.server_side_track_id.clone())
.with_play_id(play_id.clone())
.with_ssrc(ssrc)
.with_path(url)
.with_cancel_token(self.cancel_token.child_token());
if let Some(offset) = offset_ms {
file_track = file_track.with_offset_ms(offset);
}
{
let mut state = self.call_state.write().await;
state.tts_handle = None;
state.auto_hangup = match auto_hangup {
Some(true) => Some((ssrc, CallRecordHangupReason::BySystem)),
_ => None,
};
state.wait_input_timeout = wait_input_timeout;
}
self.update_track_wrapper(Box::new(file_track), play_id)
.await;
Ok(())
}
async fn do_history(&self, speaker: String, text: String) -> Result<()> {
self.event_sender
.send(SessionEvent::AddHistory {
sender: Some(self.session_id.clone()),
timestamp: crate::media::get_timestamp(),
speaker,
text,
})
.map(|_| ())
.map_err(Into::into)
}
fn do_custom(&self, sender: Option<String>, data: serde_json::Value) -> Result<()> {
self.event_sender
.send(SessionEvent::Custom {
timestamp: crate::media::get_timestamp(),
sender,
data,
})
.map(|_| ())
.map_err(Into::into)
}
async fn do_interrupt(&self, graceful: bool) -> Result<()> {
{
let mut state = self.call_state.write().await;
state.tts_handle = None;
state.moh = None;
state.auto_hangup = None;
}
self.media_stream
.remove_track(&self.server_side_track_id, graceful)
.await;
Ok(())
}
async fn do_pause(&self) -> Result<()> {
self.media_stream
.pause_playback(self.server_side_track_id.clone())
.await?;
Ok(())
}
async fn do_resume(&self) -> Result<()> {
self.media_stream
.resume_playback(self.server_side_track_id.clone())
.await?;
Ok(())
}
async fn do_hangup(
&self,
reason: Option<CallRecordHangupReason>,
initiator: Option<String>,
headers: Option<HashMap<String, String>>,
refer: Option<bool>,
) -> Result<()> {
info!(
session_id = self.session_id,
?reason,
?initiator,
?headers,
?refer,
"do_hangup"
);
let hangup_reason = match initiator.as_deref() {
Some("caller") => CallRecordHangupReason::ByCaller,
Some("callee") => CallRecordHangupReason::ByCallee,
Some("system") => CallRecordHangupReason::Autohangup,
_ => reason.unwrap_or(CallRecordHangupReason::BySystem),
};
match refer {
Some(true) => {
let (refer_state, refer_token) = {
let mut state = self.call_state.write().await;
(state.refer_callstate.clone(), state.refer_call_token.take())
};
let mut has_refer_state = false;
if let Some(refer_state) = refer_state {
has_refer_state = true;
let mut refer_state = refer_state.write().await;
if let Some(headers) = headers {
let h_val = serde_json::to_value(&headers).unwrap_or_default();
let mut extras = refer_state.extras.take().unwrap_or_default();
extras.insert("_hangup_headers".to_string(), h_val);
refer_state.extras = Some(extras);
}
refer_state.set_hangup_reason(hangup_reason);
}
if let Some(token) = refer_token {
token.cancel();
}
if has_refer_state {
self.media_stream
.remove_track(&self.server_side_track_id, false)
.await;
}
}
_ => {
let refer_token = {
let mut state = self.call_state.write().await;
if let Some(headers) = headers {
let h_val = serde_json::to_value(&headers).unwrap_or_default();
let mut extras = state.extras.take().unwrap_or_default();
extras.insert("_hangup_headers".to_string(), h_val);
state.extras = Some(extras);
}
state.set_hangup_reason(hangup_reason.clone());
state.refer_call_token.take()
};
self.media_stream
.stop(Some(hangup_reason.to_string()), initiator);
if let Some(token) = refer_token {
token.cancel();
}
}
}
tokio::task::yield_now().await;
Ok(())
}
async fn do_refer(
&self,
caller: String,
callee: String,
refer_option: Option<ReferOption>,
) -> Result<()> {
self.do_interrupt(false).await.ok();
let pause_parent_asr = refer_option
.as_ref()
.and_then(|o| o.pause_parent_asr)
.unwrap_or(false);
let original_asr_option = if pause_parent_asr {
let cs = self.call_state.read().await;
cs.option.as_ref().and_then(|o| o.asr.clone())
} else {
None
};
if pause_parent_asr {
info!(
session_id = self.session_id,
"Pausing parent call ASR during refer"
);
self.media_stream
.remove_processor::<crate::media::asr_processor::AsrProcessor>(
&self.server_side_track_id,
)
.await
.ok();
}
let mut moh = refer_option.as_ref().and_then(|o| o.moh.clone());
if let Some(ref path) = moh {
if !path.starts_with("http") && !std::path::Path::new(path).exists() {
let fallback = "./config/sounds/refer_moh.wav";
if std::path::Path::new(fallback).exists() {
info!(
session_id = self.session_id,
"moh {} not found, using fallback {}", path, fallback
);
moh = Some(fallback.to_string());
}
}
}
let ref_call_id = refer_option
.as_ref()
.and_then(|o| o.call_id.clone())
.unwrap_or_else(|| format!("ref-{}-{}", rand::random::<u32>(), self.session_id));
let session_id = self.session_id.clone();
let track_id = self.server_side_track_id.clone();
let (recorder, parent_caller) = {
let cs = self.call_state.read().await;
let option = cs.option.as_ref();
(
option.map(|o| o.recorder.clone()).unwrap_or_default(),
option.and_then(|o| o.caller.clone()),
)
};
let caller = if caller.trim().is_empty() {
parent_caller.unwrap_or_default()
} else {
caller
};
let mut call_option = CallOption {
caller: Some(caller),
callee: Some(callee.clone()),
sip: refer_option.as_ref().and_then(|o| o.sip.clone()),
vad: refer_option
.as_ref()
.and_then(|o| o.vad.clone())
.map(|mut opts| {
opts.refer = Some(true);
opts
}),
asr: refer_option
.as_ref()
.and_then(|o| o.asr.clone())
.map(|mut opts| {
opts.refer = Some(true);
opts
}),
denoise: refer_option.as_ref().and_then(|o| o.denoise.clone()),
agc: refer_option.as_ref().and_then(|o| o.agc.clone()),
recorder,
..Default::default()
};
call_option.check_default();
let mut invite_option = call_option.build_invite_option()?;
invite_option.call_id = Some(ref_call_id.clone());
let headers = invite_option.headers.get_or_insert_with(|| Vec::new());
{
let cs = self.call_state.read().await;
if let Some(opt) = cs.option.as_ref() {
if let Some(callee) = opt.callee.as_ref() {
headers.push(rsipstack::rsip::Header::Other(
"X-Referred-To".to_string(),
callee.clone(),
));
}
if let Some(caller) = opt.caller.as_ref() {
headers.push(rsipstack::rsip::Header::Other(
"X-Referred-From".to_string(),
caller.clone(),
));
}
}
}
headers.push(rsipstack::rsip::Header::Other(
"X-Referred-Id".to_string(),
self.session_id.clone(),
));
let ssrc = rand::random::<u32>();
let refer_call_state = Arc::new(RwLock::new(ActiveCallState {
session_id: ref_call_id.clone(),
start_time: Utc::now(),
ssrc,
option: Some(call_option.clone()),
is_refer: true,
..Default::default()
}));
{
let mut cs = self.call_state.write().await;
cs.refer_callstate.replace(refer_call_state.clone());
}
let auto_hangup_requested = refer_option
.as_ref()
.and_then(|o| o.auto_hangup)
.unwrap_or(true);
if auto_hangup_requested {
self.call_state.write().await.auto_hangup =
Some((ssrc, CallRecordHangupReason::ByRefer));
} else {
self.call_state.write().await.auto_hangup = None;
}
if !auto_hangup_requested && pause_parent_asr && original_asr_option.is_some() {
let asr_option = original_asr_option.unwrap();
self.call_state.write().await.pending_asr_resume = Some((ssrc, asr_option));
}
let timeout_secs = refer_option.as_ref().and_then(|o| o.timeout).unwrap_or(30);
info!(
session_id = self.session_id,
ssrc,
auto_hangup = auto_hangup_requested,
callee,
timeout_secs,
"do_refer"
);
let refer_cancel_token = self.cancel_token.child_token();
self.call_state.write().await.refer_call_token = Some(refer_cancel_token.clone());
let r = tokio::time::timeout(
Duration::from_secs(timeout_secs as u64),
self.create_outgoing_sip_track(
refer_cancel_token,
refer_call_state.clone(),
&track_id,
invite_option,
&call_option,
moh,
auto_hangup_requested,
),
)
.await;
{
self.call_state.write().await.moh = None;
}
let result = match r {
Ok(res) => res,
Err(_) => {
warn!(
session_id = session_id,
"refer sip track creation timed out after {} seconds", timeout_secs
);
self.event_sender
.send(SessionEvent::Reject {
track_id,
timestamp: crate::media::get_timestamp(),
reason: "Timeout when refer".into(),
code: Some(408),
refer: Some(true),
})
.ok();
return Err(anyhow::anyhow!("refer sip track creation timed out").into());
}
};
match result {
Ok(answer) => {
self.media_stream
.set_track_refer(&track_id, Some(true))
.await;
let forward_dtmf = refer_option
.as_ref()
.and_then(|o| o.forward_dtmf)
.unwrap_or(true);
if !forward_dtmf {
self.media_stream
.set_track_dtmf_forward(&track_id, false)
.await;
}
self.event_sender
.send(SessionEvent::Answer {
timestamp: crate::media::get_timestamp(),
track_id,
sdp: answer,
refer: Some(true),
})
.ok();
}
Err(e) => {
warn!(
session_id = session_id,
"failed to create refer sip track: {}", e
);
match &e {
rsipstack::Error::DialogError(reason, _, code) => {
self.event_sender
.send(SessionEvent::Reject {
track_id,
timestamp: crate::media::get_timestamp(),
reason: reason.clone(),
code: Some(code.code() as u32),
refer: Some(true),
})
.ok();
}
_ => {}
}
return Err(e.into());
}
}
Ok(())
}
async fn do_message(
&self,
body: String,
content_type: Option<String>,
headers: Option<HashMap<String, String>>,
refer: Option<bool>,
) -> Result<()> {
if !matches!(self.call_type, ActiveCallType::Sip | ActiveCallType::B2bua) {
return Err(anyhow::anyhow!(
"message command is only supported for SIP calls"
));
}
let dialog_key = if refer == Some(true) {
let refer_state = self.call_state.read().await.refer_callstate.clone();
match refer_state {
Some(state) => Some(state.read().await.session_id.clone()),
None => None,
}
} else {
Some(self.call_state.read().await.session_id.clone())
};
let mut dialog = dialog_key
.as_ref()
.filter(|id| !id.is_empty())
.and_then(|id| self.invitation.dialog_layer.get_dialog_with(id));
if dialog.is_none() {
if let Some(target_id) = dialog_key.as_ref().filter(|id| !id.is_empty()) {
dialog = self
.invitation
.dialog_layer
.all_dialog_ids()
.into_iter()
.filter_map(|id| self.invitation.dialog_layer.get_dialog_with(&id))
.find(|dialog| dialog.id().to_string() == *target_id);
}
}
if dialog.is_none() && refer != Some(true) {
dialog = self
.invitation
.dialog_layer
.get_client_dialog_by_call_id(&self.session_id)
.into_iter()
.find(|d| {
matches!(
d.state(),
rsipstack::dialog::dialog::DialogState::Confirmed(_, _)
)
})
.map(rsipstack::dialog::dialog::Dialog::Invite);
}
if dialog.is_none() && refer == Some(true) {
if let Some(call_id) = dialog_key.as_ref().filter(|id| !id.is_empty()) {
dialog = self
.invitation
.dialog_layer
.get_client_dialog_by_call_id(call_id)
.into_iter()
.find(|d| {
matches!(
d.state(),
rsipstack::dialog::dialog::DialogState::Confirmed(_, _)
)
})
.map(rsipstack::dialog::dialog::Dialog::Invite);
}
}
let dialog = dialog.ok_or_else(|| {
anyhow::anyhow!(
"no established SIP dialog found for message command, refer={}",
refer.unwrap_or_default()
)
})?;
let mut sip_headers = vec![rsipstack::rsip::Header::ContentType(
content_type
.clone()
.unwrap_or_else(|| "text/plain;charset=utf-8".to_string())
.into(),
)];
if let Some(headers) = headers {
sip_headers.extend(
headers
.into_iter()
.map(|(k, v)| rsipstack::rsip::Header::Other(k.into(), v.into())),
);
}
info!(
session_id = self.session_id,
dialog_id = %dialog.id(),
content_type = content_type.as_deref().unwrap_or("text/plain;charset=utf-8"),
refer = refer.unwrap_or_default(),
body = %body.chars().take(64).collect::<String>(),
"sending SIP MESSAGE"
);
let response = dialog
.message(Some(sip_headers), Some(body.into_bytes()))
.await?;
match response {
Some(resp)
if resp.status_code.kind() == rsipstack::rsip::StatusCodeKind::Successful =>
{
Ok(())
}
Some(resp) => Err(anyhow::anyhow!(
"SIP MESSAGE rejected with status {}",
resp.status_code
)),
None => Err(anyhow::anyhow!(
"SIP MESSAGE was not sent because dialog is not confirmed"
)),
}
}
fn bridge_track_id(source_session_id: &str, target_session_id: &str) -> TrackId {
format!("bridge:{}:to:{}", source_session_id, target_session_id)
}
async fn do_bridge(&self, target_session_id: String) -> Result<()> {
let target = {
let calls = self.app_state.active_calls.lock().unwrap();
calls.get(&target_session_id).cloned()
};
let target = target.ok_or_else(|| {
anyhow::anyhow!("bridge target session not found: {}", target_session_id)
})?;
if target.session_id == self.session_id {
return Err(anyhow::anyhow!("cannot bridge a call to itself").into());
}
let self_bridge_track_id = Self::bridge_track_id(&self.session_id, &target.session_id);
let target_bridge_track_id = Self::bridge_track_id(&target.session_id, &self.session_id);
self.media_stream
.remove_track(&self_bridge_track_id, false)
.await;
target
.media_stream
.remove_track(&target_bridge_track_id, false)
.await;
let (self_bridge_sender, self_bridge_receiver) = mpsc::channel(25);
let (target_bridge_sender, target_bridge_receiver) = mpsc::channel(25);
let self_paused = self.call_state.read().await.bridge_paused.clone();
let target_paused = target.call_state.read().await.bridge_paused.clone();
let self_forwarding_track = ForwardingTrack::new(
self_bridge_track_id.clone(),
self.session_id.clone(),
target_bridge_sender,
self_bridge_receiver,
self.track_config.clone(),
self.cancel_token.child_token(),
rand::random::<u32>(),
self_paused,
);
let target_forwarding_track = ForwardingTrack::new(
target_bridge_track_id.clone(),
target.session_id.clone(),
self_bridge_sender,
target_bridge_receiver,
target.track_config.clone(),
target.cancel_token.child_token(),
rand::random::<u32>(),
target_paused,
);
self.media_stream
.update_track(Box::new(self_forwarding_track), None)
.await;
target
.media_stream
.update_track(Box::new(target_forwarding_track), None)
.await;
info!(
session_id = self.session_id,
target = target_session_id,
self_bridge_track_id,
target_bridge_track_id,
"audio bridge established"
);
Ok(())
}
async fn do_unbridge(&self, target_session_id: String) -> Result<()> {
let target = {
let calls = self.app_state.active_calls.lock().unwrap();
calls.get(&target_session_id).cloned()
};
let self_bridge_track_id = Self::bridge_track_id(&self.session_id, &target_session_id);
self.media_stream
.remove_track(&self_bridge_track_id, false)
.await;
if let Some(target) = target {
let target_bridge_track_id =
Self::bridge_track_id(&target.session_id, &self.session_id);
target
.media_stream
.remove_track(&target_bridge_track_id, false)
.await;
info!(
session_id = self.session_id,
target = target.session_id,
self_bridge_track_id,
target_bridge_track_id,
"audio bridge removed"
);
} else {
info!(
session_id = self.session_id,
target = target_session_id,
self_bridge_track_id,
"audio bridge removed locally; target session not active"
);
}
Ok(())
}
async fn do_mute(&self, track_id: Option<String>) -> Result<()> {
self.media_stream.mute_track(track_id).await;
Ok(())
}
async fn do_unmute(&self, track_id: Option<String>) -> Result<()> {
self.media_stream.unmute_track(track_id).await;
Ok(())
}
pub async fn cleanup(&self) -> Result<()> {
if matches!(self.call_type, ActiveCallType::Sip | ActiveCallType::B2bua) {
self.do_reject(
Some(rsipstack::rsip::StatusCode::Decline),
Some("handler disconnected".to_string()),
)
.await
.ok();
}
self.call_state.write().await.tts_handle = None;
self.media_stream.cleanup().await.ok();
Ok(())
}
pub fn get_callrecord(&self) -> Option<CallRecord> {
self.call_state.try_read().ok().map(|call_state| {
call_state.build_callrecord(
self.app_state.clone(),
self.session_id.clone(),
self.call_type.clone(),
)
})
}
async fn dump_to_file(
&self,
dump_file: &mut File,
cmd_receiver: &mut CommandReceiver,
event_receiver: &mut EventReceiver,
) {
loop {
select! {
_ = self.cancel_token.cancelled() => {
break;
}
Ok(cmd) = cmd_receiver.recv() => {
CallRecordEvent::write(CallRecordEventType::Command, cmd, dump_file)
.await;
}
Ok(event) = event_receiver.recv() => {
if matches!(event, SessionEvent::Binary{..}) {
continue;
}
CallRecordEvent::write(CallRecordEventType::Event, event, dump_file)
.await;
}
};
}
}
async fn dump_loop(
&self,
dump_events: bool,
mut dump_cmd_receiver: CommandReceiver,
mut dump_event_receiver: EventReceiver,
) {
if !dump_events {
return;
}
let file_name = self.app_state.get_dump_events_file(&self.session_id);
let mut dump_file = match File::options()
.create(true)
.append(true)
.open(&file_name)
.await
{
Ok(file) => file,
Err(e) => {
warn!(
session_id = self.session_id,
file_name, "failed to open dump events file: {}", e
);
return;
}
};
self.dump_to_file(
&mut dump_file,
&mut dump_cmd_receiver,
&mut dump_event_receiver,
)
.await;
while let Ok(event) = dump_event_receiver.try_recv() {
if matches!(event, SessionEvent::Binary { .. }) {
continue;
}
CallRecordEvent::write(CallRecordEventType::Event, event, &mut dump_file).await;
}
}
pub async fn create_rtp_track(
&self,
track_id: TrackId,
ssrc: u32,
enable_srtp: Option<bool>,
) -> Result<RtcTrack> {
let mut rtc_config = RtcTrackConfig::default();
let use_srtp = enable_srtp
.or(self.app_state.config.enable_srtp)
.unwrap_or(false);
rtc_config.mode = if use_srtp {
rustrtc::TransportMode::Srtp
} else {
rustrtc::TransportMode::Rtp
};
if let Some(codecs) = &self.app_state.config.codecs {
let mut codec_types = Vec::new();
for c in codecs {
match c.to_lowercase().as_str() {
"pcmu" => codec_types.push(CodecType::PCMU),
"pcma" => codec_types.push(CodecType::PCMA),
"g722" => codec_types.push(CodecType::G722),
"g729" => codec_types.push(CodecType::G729),
"opus" => codec_types.push(CodecType::Opus),
"dtmf" | "2833" | "telephone_event" => {
codec_types.push(CodecType::TelephoneEvent)
}
_ => {}
}
}
if !codec_types.is_empty() {
rtc_config.preferred_codec = Some(codec_types[0].clone());
rtc_config.codecs = codec_types;
}
}
if rtc_config.preferred_codec.is_none() {
rtc_config.preferred_codec = Some(self.track_config.codec.clone());
}
rtc_config.rtp_port_range = self
.app_state
.config
.rtp_start_port
.zip(self.app_state.config.rtp_end_port);
if let Some(ref external_ip) = self.app_state.config.external_ip {
rtc_config.external_ip = Some(external_ip.clone());
}
if let Some(ref bind_ip) = self.app_state.config.rtp_bind_ip {
rtc_config.bind_ip = Some(bind_ip.clone());
}
rtc_config.enable_latching = self.app_state.config.enable_rtp_latching;
rtc_config.enable_ice_lite = self
.call_state
.read()
.await
.option
.as_ref()
.and_then(|o| o.enable_ice_lite)
.or(self.app_state.config.enable_ice_lite);
let mut track = RtcTrack::new(
self.cancel_token.child_token(),
track_id,
self.track_config.clone(),
rtc_config,
)
.with_ssrc(ssrc);
track.create().await?;
Ok(track)
}
async fn setup_caller_track(&self, option: &CallOption) -> Result<()> {
let hangup_headers = option
.sip
.as_ref()
.and_then(|s| s.hangup_headers.as_ref())
.map(|headers_map| {
headers_map
.iter()
.map(|(k, v)| rsipstack::rsip::Header::Other(k.clone(), v.clone()))
.collect::<Vec<rsipstack::rsip::Header>>()
});
self.call_state.write().await.option = Some(option.clone());
info!(
session_id = self.session_id,
call_type = ?self.call_type,
"setup caller track"
);
let track = match self.call_type {
ActiveCallType::Webrtc => Some(self.create_webrtc_track().await?),
ActiveCallType::WebSocket => {
let audio_receiver = self.call_state.write().await.audio_receiver.take();
if let Some(receiver) = audio_receiver {
Some(self.create_websocket_track(receiver).await?)
} else {
None
}
}
ActiveCallType::Sip => {
if let Some(dialog_id) = self
.invitation
.find_dialog_id_by_session_id(&self.session_id)
{
if let Some(pending_dialog) = self.invitation.get_pending_call(&dialog_id) {
return self
.prepare_incoming_sip_track(
self.cancel_token.clone(),
self.call_state.clone(),
&self.session_id,
pending_dialog,
hangup_headers,
)
.await;
}
}
let mut option = option.clone();
if option.sip.is_none()
|| option
.sip
.as_ref()
.and_then(|s| s.username.as_ref())
.is_none()
{
if let Some(callee) = &option.callee {
if let Some(cred) = self.app_state.find_credentials_for_callee(callee) {
if option.sip.is_none() {
option.sip = Some(crate::SipOption {
username: Some(cred.username.clone()),
password: Some(cred.password.clone()),
realm: cred.realm.clone(),
..Default::default()
});
}
}
}
}
let mut invite_option = option.build_invite_option()?;
invite_option.call_id = Some(self.session_id.clone());
match self
.create_outgoing_sip_track(
self.cancel_token.clone(),
self.call_state.clone(),
&self.session_id,
invite_option,
&option,
None,
false,
)
.await
{
Ok(answer) => {
self.event_sender
.send(SessionEvent::Answer {
timestamp: crate::media::get_timestamp(),
track_id: self.session_id.clone(),
sdp: answer,
refer: Some(false),
})
.ok();
return Ok(());
}
Err(e) => {
warn!(
session_id = self.session_id,
"failed to create sip track: {}", e
);
match &e {
rsipstack::Error::DialogError(reason, _, code) => {
self.event_sender
.send(SessionEvent::Reject {
track_id: self.session_id.clone(),
timestamp: crate::media::get_timestamp(),
reason: reason.clone(),
code: Some(code.code() as u32),
refer: Some(false),
})
.ok();
}
_ => {}
}
return Err(e.into());
}
}
}
ActiveCallType::B2bua => {
if let Some(dialog_id) = self
.invitation
.find_dialog_id_by_session_id(&self.session_id)
{
if let Some(pending_dialog) = self.invitation.get_pending_call(&dialog_id) {
return self
.prepare_incoming_sip_track(
self.cancel_token.clone(),
self.call_state.clone(),
&self.session_id,
pending_dialog,
hangup_headers,
)
.await;
}
}
warn!(
session_id = self.session_id,
"no pending dialog found for B2BUA call"
);
return Err(anyhow::anyhow!(
"no pending dialog found for session_id: {}",
self.session_id
));
}
};
match track {
Some(track) => {
self.finish_caller_stack(&option, PendingCallerTrack::NotStarted(track))
.await?;
}
None => {
warn!(session_id = self.session_id, "no track created for caller");
return Err(anyhow::anyhow!("no track created for caller"));
}
}
Ok(())
}
async fn finish_caller_stack(
&self,
option: &CallOption,
pending_track: PendingCallerTrack,
) -> Result<()> {
match pending_track {
PendingCallerTrack::NotStarted(track) => {
self.setup_track_with_stream(option, track).await?;
}
PendingCallerTrack::StartedForEarlyMedia => {
let track_id = self.session_id.clone();
let processors = StreamEngine::create_processors(
self.app_state.stream_engine.clone(),
track_id.clone(),
self.cancel_token.child_token(),
self.event_sender.clone(),
self.media_stream.packet_sender.clone(),
option,
)
.await
.unwrap_or_else(|e| {
warn!(
session_id = self.session_id,
"failed to create processors on accept: {}", e
);
vec![]
});
for processor in processors {
self.media_stream
.append_processor(&track_id, processor)
.await
.ok();
}
}
}
{
let call_state = self.call_state.read().await;
if let Some(ref answer) = call_state.answer {
info!(
session_id = self.session_id,
"sending answer event: {}", answer,
);
self.event_sender
.send(SessionEvent::Answer {
timestamp: crate::media::get_timestamp(),
track_id: self.session_id.clone(),
sdp: answer.clone(),
refer: Some(false),
})
.ok();
} else {
warn!(
session_id = self.session_id,
"no answer in state to send event"
);
}
}
Ok(())
}
pub async fn setup_track_with_stream(
&self,
option: &CallOption,
mut track: Box<dyn Track>,
) -> Result<()> {
let processors = match StreamEngine::create_processors(
self.app_state.stream_engine.clone(),
track.id().clone(),
self.cancel_token.child_token(),
self.event_sender.clone(),
self.media_stream.packet_sender.clone(),
option,
)
.await
{
Ok(processors) => processors,
Err(e) => {
warn!(
session_id = self.session_id,
"failed to prepare stream processors: {}", e
);
vec![]
}
};
for processor in processors {
track.append_processor(processor);
}
self.update_track_wrapper(track, None).await;
Ok(())
}
pub async fn update_track_wrapper(&self, mut track: Box<dyn Track>, play_id: Option<String>) {
let (ambiance_opt, subscribe) = {
let state = self.call_state.read().await;
let mut opt = state
.option
.as_ref()
.and_then(|o| o.ambiance.clone())
.unwrap_or_default();
if let Some(global) = &self.app_state.config.ambiance {
opt.merge(global);
}
let subscribe = state
.option
.as_ref()
.and_then(|o| o.subscribe)
.unwrap_or_default();
(opt, subscribe)
};
if track.id() == &self.server_side_track_id && ambiance_opt.path.is_some() {
match AmbianceProcessor::new(ambiance_opt).await {
Ok(ambiance) => {
info!(session_id = self.session_id, "loaded ambiance processor");
track.append_processor(Box::new(ambiance));
}
Err(e) => {
tracing::error!("failed to load ambiance wav {}", e);
}
}
}
if subscribe && self.call_type != ActiveCallType::WebSocket {
let (track_index, sub_track_id) = if track.id() == &self.server_side_track_id {
(0, self.server_side_track_id.clone())
} else {
(1, self.session_id.clone())
};
let sub_processor =
SubscribeProcessor::new(self.event_sender.clone(), sub_track_id, track_index);
track.append_processor(Box::new(sub_processor));
}
self.call_state.write().await.current_play_id = play_id.clone();
self.media_stream.update_track(track, play_id).await;
}
pub async fn create_websocket_track(
&self,
audio_receiver: WebsocketBytesReceiver,
) -> Result<Box<dyn Track>> {
let (ssrc, codec) = {
let call_state = self.call_state.read().await;
(
call_state.ssrc,
call_state
.option
.as_ref()
.map(|o| o.codec.clone())
.unwrap_or_default(),
)
};
let ws_track = WebsocketTrack::new(
self.cancel_token.child_token(),
self.session_id.clone(),
self.track_config.clone(),
self.event_sender.clone(),
audio_receiver,
codec,
ssrc,
);
{
let mut call_state = self.call_state.write().await;
call_state.answer_time = Some(Utc::now());
call_state.answer = Some("".to_string());
call_state.last_status_code = 200;
}
Ok(Box::new(ws_track))
}
pub(super) async fn create_webrtc_track(&self) -> Result<Box<dyn Track>> {
let (ssrc, option) = {
let call_state = self.call_state.read().await;
(
call_state.ssrc,
call_state.option.clone().unwrap_or_default(),
)
};
let mut rtc_config = RtcTrackConfig::default();
rtc_config.mode = rustrtc::TransportMode::WebRtc; rtc_config.ice_servers = self.app_state.config.ice_servers.clone();
if let Some(codecs) = &self.app_state.config.codecs {
let mut codec_types = Vec::new();
for c in codecs {
match c.to_lowercase().as_str() {
"pcmu" => codec_types.push(CodecType::PCMU),
"pcma" => codec_types.push(CodecType::PCMA),
"g722" => codec_types.push(CodecType::G722),
"g729" => codec_types.push(CodecType::G729),
"opus" => codec_types.push(CodecType::Opus),
"dtmf" | "2833" | "telephone_event" => {
codec_types.push(CodecType::TelephoneEvent)
}
_ => {}
}
}
if !codec_types.is_empty() {
rtc_config.preferred_codec = Some(codec_types[0].clone());
rtc_config.codecs = codec_types;
}
}
if let Some(ref external_ip) = self.app_state.config.external_ip {
rtc_config.external_ip = Some(external_ip.clone());
}
if let Some(ref bind_ip) = self.app_state.config.rtp_bind_ip {
rtc_config.bind_ip = Some(bind_ip.clone());
}
let mut webrtc_track = RtcTrack::new(
self.cancel_token.child_token(),
self.session_id.clone(),
self.track_config.clone(),
rtc_config,
)
.with_ssrc(ssrc);
let timeout = option.handshake_timeout.map(|t| Duration::from_secs(t));
let offer = match option.enable_ipv6 {
Some(false) | None => {
strip_ipv6_candidates(option.offer.as_ref().unwrap_or(&"".to_string()))
}
_ => option.offer.clone().unwrap_or("".to_string()),
};
let answer: Option<String>;
match webrtc_track.handshake(offer, timeout).await {
Ok(answer_sdp) => {
answer = match option.enable_ipv6 {
Some(false) | None => Some(strip_ipv6_candidates(&answer_sdp)),
Some(true) => Some(answer_sdp.to_string()),
};
}
Err(e) => {
warn!(session_id = self.session_id, "failed to setup track: {}", e);
return Err(anyhow::anyhow!("Failed to setup track: {}", e));
}
}
{
let mut call_state = self.call_state.write().await;
call_state.answer_time = Some(Utc::now());
call_state.answer = answer;
call_state.last_status_code = 200;
}
Ok(Box::new(webrtc_track))
}
async fn create_outgoing_sip_track(
&self,
cancel_token: CancellationToken,
call_state_ref: ActiveCallStateRef,
track_id: &String,
mut invite_option: InviteOption,
call_option: &CallOption,
moh: Option<String>,
auto_hangup: bool,
) -> Result<String, rsipstack::Error> {
self.app_state.config.apply_trunk_rules(&mut invite_option);
let ssrc = call_state_ref.read().await.ssrc;
let per_call_srtp = call_option.sip.as_ref().and_then(|s| s.enable_srtp);
let rtp_track = self
.create_rtp_track(track_id.clone(), ssrc, per_call_srtp)
.await
.map_err(|e| rsipstack::Error::Error(e.to_string()))?;
let offer = Some(
rtp_track
.local_description()
.await
.map_err(|e| rsipstack::Error::Error(e.to_string()))?,
);
{
let mut cs = call_state_ref.write().await;
if let Some(o) = cs.option.as_mut() {
o.offer = offer.clone();
}
cs.start_time = Utc::now();
};
invite_option.offer = offer.clone().map(|s| s.into());
let needs_contact = contact_needs_public_resolution(&invite_option.contact);
if needs_contact {
let addrs = self.invitation.dialog_layer.endpoint.get_addrs();
if let Some(addr) = find_local_addr_for_uri(&addrs, &invite_option.callee) {
let contact_username = invite_option
.contact
.auth
.as_ref()
.map(|auth| auth.user.as_str())
.or_else(|| {
invite_option
.caller
.auth
.as_ref()
.map(|auth| auth.user.as_str())
});
invite_option.contact = build_public_contact_uri(
&self.app_state.learned_public_address,
self.app_state.auto_learn_public_address_enabled(),
&addr,
contact_username,
Some(&invite_option.contact),
);
} else {
return Err(rsipstack::Error::Error(format!(
"missing local SIP address for callee transport: {}",
invite_option.callee
)));
}
}
let mut rtp_track_to_setup = Some(Box::new(rtp_track) as Box<dyn Track>);
if let Some(moh) = moh {
let ssrc_and_moh = {
let mut state = call_state_ref.write().await;
state.moh = Some(moh.clone());
if state.current_play_id.is_none() {
let ssrc = rand::random::<u32>();
Some((ssrc, moh.clone()))
} else {
info!(
session_id = self.session_id,
"Something is playing, MOH will start after it ends"
);
None
}
};
if let Some((ssrc, moh_path)) = ssrc_and_moh {
let file_track = FileTrack::new(self.server_side_track_id.clone())
.with_play_id(Some(moh_path.clone()))
.with_ssrc(ssrc)
.with_path(moh_path.clone())
.with_cancel_token(self.cancel_token.child_token());
self.update_track_wrapper(Box::new(file_track), Some(moh_path))
.await;
}
} else {
let track = rtp_track_to_setup.take().unwrap();
self.setup_track_with_stream(&call_option, track)
.await
.map_err(|e| rsipstack::Error::Error(e.to_string()))?;
}
info!(
session_id = self.session_id,
track_id,
contact = %invite_option.contact,
"invite {} -> {} offer: \n{}",
invite_option.caller,
invite_option.callee,
offer.as_ref().map(|s| s.as_str()).unwrap_or("<NO OFFER>")
);
let (dlg_state_sender, dlg_state_receiver) =
self.invitation.dialog_layer.new_dialog_state_channel();
let states = InviteDialogStates {
is_client: true,
session_id: self.session_id.clone(),
track_id: track_id.clone(),
event_sender: self.event_sender.clone(),
media_stream: self.media_stream.clone(),
call_state: call_state_ref.clone(),
cancel_token,
terminated_reason: None,
has_early_media: false,
};
let hangup_headers = call_option
.sip
.as_ref()
.and_then(|s| s.hangup_headers.as_ref())
.map(|headers_map| {
headers_map
.iter()
.map(|(k, v)| rsipstack::rsip::Header::Other(k.clone(), v.clone()))
.collect::<Vec<rsipstack::rsip::Header>>()
});
let mut client_dialog_handler = DialogStateReceiverGuard::new(
self.invitation.dialog_layer.clone(),
dlg_state_receiver,
hangup_headers,
);
crate::spawn(async move {
client_dialog_handler.process_dialog(states).await;
});
let (dialog_id, answer) = self
.invitation
.invite(invite_option, dlg_state_sender)
.await?;
self.call_state.write().await.moh = None;
if let Some(track) = rtp_track_to_setup {
info!(
session_id = self.session_id,
track_id, "Stopping MOH and setting up RTP track"
);
self.media_stream
.remove_track(&self.server_side_track_id, false)
.await;
self.setup_track_with_stream(&call_option, track)
.await
.map_err(|e| rsipstack::Error::Error(e.to_string()))?;
}
let answer = match answer {
Some(answer) => {
let s = String::from_utf8_lossy(&answer).to_string();
if s.trim().is_empty() {
let cs = call_state_ref.read().await;
match cs.answer.clone() {
Some(early_sdp) if !early_sdp.is_empty() => {
info!(
session_id = self.session_id,
"200 OK has empty body; using early-media SDP from 183"
);
(early_sdp, true )
}
_ => {
warn!(
session_id = self.session_id,
"200 OK has empty body and no early-media SDP available"
);
(s, false)
}
}
} else {
(s, false)
}
}
None => {
let cs = call_state_ref.read().await;
match cs.answer.clone() {
Some(early_sdp) if !early_sdp.is_empty() => {
info!(
session_id = self.session_id,
"200 OK had no answer; using early-media SDP from 183"
);
(early_sdp, true )
}
_ => {
warn!(session_id = self.session_id, "no answer received");
return Err(rsipstack::Error::DialogError(
"No answer received".to_string(),
dialog_id,
rsipstack::rsip::StatusCode::NotAcceptableHere,
));
}
}
}
};
let (answer, remote_description_already_applied) = answer;
{
let mut cs = call_state_ref.write().await;
if cs.answer.is_none() {
cs.answer = Some(answer.clone());
}
if auto_hangup {
cs.auto_hangup = Some((ssrc, CallRecordHangupReason::ByRefer));
}
}
if !remote_description_already_applied {
self.media_stream
.update_remote_description(&track_id, &answer)
.await
.ok();
}
Ok(answer)
}
pub fn is_webrtc_sdp(sdp: &str) -> bool {
(sdp.contains("a=ice-ufrag:") || sdp.contains("a=ice-pwd:"))
&& sdp.contains("a=fingerprint:")
}
pub async fn setup_answer_track(
&self,
ssrc: u32,
option: &CallOption,
offer: String,
) -> Result<(String, Box<dyn Track>)> {
let offer = match option.enable_ipv6 {
Some(false) | None => strip_ipv6_candidates(&offer),
_ => offer.clone(),
};
let timeout = option.handshake_timeout.map(|t| Duration::from_secs(t));
let mut media_track = if Self::is_webrtc_sdp(&offer) {
let mut rtc_config = RtcTrackConfig::default();
rtc_config.mode = rustrtc::TransportMode::WebRtc;
rtc_config.ice_servers = self.app_state.config.ice_servers.clone();
if let Some(ref external_ip) = self.app_state.config.external_ip {
rtc_config.external_ip = Some(external_ip.clone());
}
if let Some(ref bind_ip) = self.app_state.config.rtp_bind_ip {
rtc_config.bind_ip = Some(bind_ip.clone());
}
rtc_config.enable_latching = self.app_state.config.enable_rtp_latching;
rtc_config.enable_ice_lite = self
.call_state
.read()
.await
.option
.as_ref()
.and_then(|o| o.enable_ice_lite)
.or(self.app_state.config.enable_ice_lite);
let webrtc_track = RtcTrack::new(
self.cancel_token.child_token(),
self.session_id.clone(),
self.track_config.clone(),
rtc_config,
)
.with_ssrc(ssrc);
Box::new(webrtc_track) as Box<dyn Track>
} else {
let per_call_srtp = option.sip.as_ref().and_then(|s| s.enable_srtp);
let rtp_track = self
.create_rtp_track(self.session_id.clone(), ssrc, per_call_srtp)
.await?;
Box::new(rtp_track) as Box<dyn Track>
};
let answer = match media_track.handshake(offer.clone(), timeout).await {
Ok(answer) => answer,
Err(e) => {
return Err(anyhow::anyhow!("handshake failed: {e}"));
}
};
return Ok((answer, media_track));
}
pub async fn prepare_incoming_sip_track(
&self,
cancel_token: CancellationToken,
call_state_ref: ActiveCallStateRef,
track_id: &String,
pending_dialog: PendingDialog,
hangup_headers: Option<Vec<rsipstack::rsip::Header>>,
) -> Result<()> {
let state_receiver = pending_dialog.state_receiver;
let states = InviteDialogStates {
is_client: false,
session_id: self.session_id.clone(),
track_id: track_id.clone(),
event_sender: self.event_sender.clone(),
media_stream: self.media_stream.clone(),
call_state: self.call_state.clone(),
cancel_token,
terminated_reason: None,
has_early_media: false,
};
let initial_request = pending_dialog.dialog.initial_request();
let offer = String::from_utf8_lossy(&initial_request.body).to_string();
let caller = initial_request
.from_header()
.ok()
.and_then(|h| h.uri().ok())
.map(|u| u.to_string())
.unwrap_or_default();
let callee = initial_request
.to_header()
.ok()
.and_then(|h| h.uri().ok())
.map(|u| u.to_string())
.unwrap_or_default();
let headers: Option<HashMap<String, String>> = {
let mut h = HashMap::new();
for header in initial_request.headers.iter() {
if let rsipstack::rsip::Header::Other(name, value) = header {
h.insert(name.to_string(), value.to_string());
}
}
if h.is_empty() { None } else { Some(h) }
};
self.event_sender
.send(SessionEvent::Incoming {
track_id: self.session_id.clone(),
timestamp: crate::media::get_timestamp(),
caller,
callee,
sdp: offer.clone(),
headers,
})
.ok();
let (ssrc, option) = {
let call_state = call_state_ref.read().await;
(
call_state.ssrc,
call_state.option.clone().unwrap_or_default(),
)
};
match self.setup_answer_track(ssrc, &option, offer).await {
Ok((offer, track)) => {
self.update_track_wrapper(track, None).await;
let mut state = self.call_state.write().await;
state.ready_to_answer = Some((
offer,
PendingCallerTrack::StartedForEarlyMedia,
pending_dialog.dialog,
));
}
Err(e) => {
return Err(anyhow::anyhow!("error creating track: {}", e));
}
}
let mut client_dialog_handler = DialogStateReceiverGuard::new(
self.invitation.dialog_layer.clone(),
state_receiver,
hangup_headers,
);
crate::spawn(async move {
client_dialog_handler.process_dialog(states).await;
});
Ok(())
}
}
impl Drop for ActiveCall {
fn drop(&mut self) {
info!(session_id = self.session_id, "dropping active call");
if let Some(sender) = self.app_state.callrecord_sender.as_ref() {
if let Some(record) = self.get_callrecord() {
if let Err(e) = sender.send(record) {
warn!(
session_id = self.session_id,
"failed to send call record: {}", e
);
}
}
}
}
}
impl ActiveCallState {
pub fn merge_option(&self, mut option: CallOption) -> CallOption {
if let Some(existing) = &self.option {
if option.asr.is_none() {
option.asr = existing.asr.clone();
}
if option.tts.is_none() {
option.tts = existing.tts.clone();
}
if option.vad.is_none() {
option.vad = existing.vad.clone();
}
if option.denoise.is_none() {
option.denoise = existing.denoise;
}
if option.agc.is_none() {
option.agc = existing.agc.clone();
}
if option.recorder.is_none() {
option.recorder = existing.recorder.clone();
}
if option.eou.is_none() {
option.eou = existing.eou.clone();
}
if option.extra.is_none() {
option.extra = existing.extra.clone();
}
if option.ambiance.is_none() {
option.ambiance = existing.ambiance.clone();
}
if option.ringback_detection.is_none() {
option.ringback_detection = existing.ringback_detection.clone();
}
}
option
}
pub fn set_hangup_reason(&mut self, reason: CallRecordHangupReason) {
if self.hangup_reason.is_none() {
self.hangup_reason = Some(reason);
}
}
pub fn build_hangup_event(
&self,
track_id: TrackId,
initiator: Option<String>,
) -> crate::event::SessionEvent {
let from = self.option.as_ref().and_then(|o| o.caller.as_ref());
let to = self.option.as_ref().and_then(|o| o.callee.as_ref());
let extra = self.extras.clone();
crate::event::SessionEvent::Hangup {
track_id,
timestamp: crate::media::get_timestamp(),
reason: Some(format!("{:?}", self.hangup_reason)),
initiator,
start_time: self.start_time.to_rfc3339(),
answer_time: self.answer_time.map(|t| t.to_rfc3339()),
ringing_time: self.ring_time.map(|t| t.to_rfc3339()),
hangup_time: Utc::now().to_rfc3339(),
extra,
from: from.map(|f| f.into()),
to: to.map(|f| f.into()),
refer: Some(self.is_refer),
}
}
pub fn build_callrecord(
&self,
app_state: AppState,
session_id: String,
call_type: ActiveCallType,
) -> CallRecord {
let option = self.option.clone().unwrap_or_default();
let recorder = if option.recorder.is_some() {
let recorder_file = app_state.get_recorder_file(&session_id);
if std::path::Path::new(&recorder_file).exists() {
let file_size = std::fs::metadata(&recorder_file)
.map(|m| m.len())
.unwrap_or(0);
vec![crate::callrecord::CallRecordMedia {
track_id: session_id.clone(),
path: recorder_file,
size: file_size,
extra: None,
}]
} else {
vec![]
}
} else {
vec![]
};
let dump_event_file = app_state.get_dump_events_file(&session_id);
let dump_event_file = if std::path::Path::new(&dump_event_file).exists() {
Some(dump_event_file)
} else {
None
};
let refer_callrecord = self.refer_callstate.as_ref().and_then(|rc| {
if let Ok(rc) = rc.try_read() {
Some(Box::new(rc.build_callrecord(
app_state.clone(),
rc.session_id.clone(),
ActiveCallType::B2bua,
)))
} else {
None
}
});
let caller = option.caller.clone().unwrap_or_default();
let callee = option.callee.clone().unwrap_or_default();
CallRecord {
option: Some(option),
call_id: session_id,
call_type,
start_time: self.start_time,
ring_time: self.ring_time.clone(),
answer_time: self.answer_time.clone(),
end_time: Utc::now(),
caller,
callee,
hangup_reason: self.hangup_reason.clone(),
hangup_messages: Vec::new(),
status_code: self.last_status_code,
extras: self.extras.clone(),
dump_event_file,
recorder,
refer_callrecord,
}
}
}