mirror of
https://github.com/coral/sipcord-bridge.git
synced 2026-07-01 18:32:49 -06:00
dont wait for menu to end befor button press accepts
This commit is contained in:
parent
2630ca154f
commit
4d8dd36538
|
|
@ -1598,14 +1598,8 @@ async fn select_guild_from_menu(
|
||||||
page,
|
page,
|
||||||
guilds.len(),
|
guilds.len(),
|
||||||
);
|
);
|
||||||
if let Err(e) = play_tts_prompt(call_id, &prompt, &ctx.sip_cmd_tx).await {
|
let digit =
|
||||||
error!("Failed to play guild menu TTS for call {}: {}", call_id, e);
|
play_tts_prompt_and_wait_for_digit(call_id, menu, &prompt, dtmf_rx, ctx).await?;
|
||||||
ctx.dtmf_waiters.remove(&call_id);
|
|
||||||
let _ = ctx.sip_cmd_tx.send(SipCommand::Hangup { call_id });
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let digit = wait_for_menu_digit(call_id, menu, dtmf_rx, ctx).await?;
|
|
||||||
match digit {
|
match digit {
|
||||||
'#' => continue,
|
'#' => continue,
|
||||||
'9' if has_next_page(guilds.len(), page) => {
|
'9' if has_next_page(guilds.len(), page) => {
|
||||||
|
|
@ -1659,14 +1653,8 @@ async fn select_channel_from_menu(
|
||||||
page,
|
page,
|
||||||
channels.len(),
|
channels.len(),
|
||||||
);
|
);
|
||||||
if let Err(e) = play_tts_prompt(call_id, &prompt, &ctx.sip_cmd_tx).await {
|
let digit =
|
||||||
error!("Failed to play channel menu TTS for call {}: {}", call_id, e);
|
play_tts_prompt_and_wait_for_digit(call_id, menu, &prompt, dtmf_rx, ctx).await?;
|
||||||
ctx.dtmf_waiters.remove(&call_id);
|
|
||||||
let _ = ctx.sip_cmd_tx.send(SipCommand::Hangup { call_id });
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let digit = wait_for_menu_digit(call_id, menu, dtmf_rx, ctx).await?;
|
|
||||||
match digit {
|
match digit {
|
||||||
'#' => continue,
|
'#' => continue,
|
||||||
'*' if page > 0 => {
|
'*' if page > 0 => {
|
||||||
|
|
@ -1782,9 +1770,10 @@ fn is_tts_skipped_symbol(ch: char) -> bool {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_menu_digit(
|
async fn play_tts_prompt_and_wait_for_digit(
|
||||||
call_id: CallId,
|
call_id: CallId,
|
||||||
menu: &MenuRoute,
|
menu: &MenuRoute,
|
||||||
|
text: &str,
|
||||||
dtmf_rx: &mut mpsc::UnboundedReceiver<char>,
|
dtmf_rx: &mut mpsc::UnboundedReceiver<char>,
|
||||||
ctx: &MenuCallContext,
|
ctx: &MenuCallContext,
|
||||||
) -> Option<char> {
|
) -> Option<char> {
|
||||||
|
|
@ -1793,13 +1782,28 @@ async fn wait_for_menu_digit(
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
match tokio::time::timeout(
|
let samples = match synthesize_tts_samples(call_id, text).await {
|
||||||
Duration::from_secs(menu.timeout_seconds.max(1)),
|
Ok(samples) => samples,
|
||||||
dtmf_rx.recv(),
|
Err(e) => {
|
||||||
)
|
error!("Failed to synthesize menu TTS for call {}: {}", call_id, e);
|
||||||
.await
|
ctx.dtmf_waiters.remove(&call_id);
|
||||||
{
|
let _ = ctx.sip_cmd_tx.send(SipCommand::Hangup { call_id });
|
||||||
Ok(Some(digit)) => Some(digit),
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let duration_ms = (samples.len() as u64 * 1000) / CONF_SAMPLE_RATE as u64;
|
||||||
|
let _ = ctx
|
||||||
|
.sip_cmd_tx
|
||||||
|
.send(SipCommand::PlayDirectToCall { call_id, samples });
|
||||||
|
|
||||||
|
let wait_duration =
|
||||||
|
Duration::from_millis(duration_ms + 100) + Duration::from_secs(menu.timeout_seconds.max(1));
|
||||||
|
match tokio::time::timeout(wait_duration, dtmf_rx.recv()).await {
|
||||||
|
Ok(Some(digit)) => {
|
||||||
|
let _ = ctx.sip_cmd_tx.send(SipCommand::StopDirectToCall { call_id });
|
||||||
|
Some(digit)
|
||||||
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
warn!("Menu {} DTMF channel closed for call {}", menu.id, call_id);
|
warn!("Menu {} DTMF channel closed for call {}", menu.id, call_id);
|
||||||
ctx.dtmf_waiters.remove(&call_id);
|
ctx.dtmf_waiters.remove(&call_id);
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,9 @@ fn describe_op(op: &PendingPjsuaOp) -> String {
|
||||||
PendingPjsuaOp::PlayDirect { call_id, samples } => {
|
PendingPjsuaOp::PlayDirect { call_id, samples } => {
|
||||||
format!("PlayDirect {{ call_id: {}, samples: {} }}", call_id, samples.len())
|
format!("PlayDirect {{ call_id: {}, samples: {} }}", call_id, samples.len())
|
||||||
}
|
}
|
||||||
|
PendingPjsuaOp::StopDirect { call_id } => {
|
||||||
|
format!("StopDirect {{ call_id: {} }}", call_id)
|
||||||
|
}
|
||||||
PendingPjsuaOp::StartLoop { call_id, samples } => {
|
PendingPjsuaOp::StartLoop { call_id, samples } => {
|
||||||
format!("StartLoop {{ call_id: {}, samples: {} }}", call_id, samples.len())
|
format!("StartLoop {{ call_id: {}, samples: {} }}", call_id, samples.len())
|
||||||
}
|
}
|
||||||
|
|
@ -355,6 +358,7 @@ fn process_pending_pjsua_ops() {
|
||||||
// Validate that the call still exists before processing the op
|
// Validate that the call still exists before processing the op
|
||||||
let call_id = match &op {
|
let call_id = match &op {
|
||||||
PendingPjsuaOp::PlayDirect { call_id, .. } => Some(*call_id),
|
PendingPjsuaOp::PlayDirect { call_id, .. } => Some(*call_id),
|
||||||
|
PendingPjsuaOp::StopDirect { call_id } => Some(*call_id),
|
||||||
PendingPjsuaOp::StartLoop { call_id, .. } => Some(*call_id),
|
PendingPjsuaOp::StartLoop { call_id, .. } => Some(*call_id),
|
||||||
PendingPjsuaOp::StartStreaming { call_id, .. } => Some(*call_id),
|
PendingPjsuaOp::StartStreaming { call_id, .. } => Some(*call_id),
|
||||||
PendingPjsuaOp::StartTestTone { call_id } => Some(*call_id),
|
PendingPjsuaOp::StartTestTone { call_id } => Some(*call_id),
|
||||||
|
|
@ -387,6 +391,9 @@ fn process_pending_pjsua_ops() {
|
||||||
tracing::warn!("Failed to play direct audio to call {}: {}", call_id, e);
|
tracing::warn!("Failed to play direct audio to call {}: {}", call_id, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PendingPjsuaOp::StopDirect { call_id } => {
|
||||||
|
super::ffi::direct_player::stop_direct_audio_to_call_internal(call_id);
|
||||||
|
}
|
||||||
PendingPjsuaOp::StartStreaming {
|
PendingPjsuaOp::StartStreaming {
|
||||||
call_id,
|
call_id,
|
||||||
path,
|
path,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use crate::transport::sip::error::SipAudioError;
|
||||||
use super::types::*;
|
use super::types::*;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use pjsua::*;
|
use pjsua::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
/// Custom get_frame callback for direct player ports
|
/// Custom get_frame callback for direct player ports
|
||||||
/// Returns samples from the player's buffer, advancing position each call
|
/// Returns samples from the player's buffer, advancing position each call
|
||||||
|
|
@ -73,6 +73,20 @@ pub unsafe extern "C" fn direct_player_on_destroy(this_port: *mut pjmedia_port)
|
||||||
if let Some(state) = DIRECT_PLAYER_STATE.get() {
|
if let Some(state) = DIRECT_PLAYER_STATE.get() {
|
||||||
state.lock().remove(&port_key);
|
state.lock().remove(&port_key);
|
||||||
}
|
}
|
||||||
|
let call_id = DIRECT_PLAYER_CALLS
|
||||||
|
.get()
|
||||||
|
.and_then(|calls| calls.lock().remove(&port_key));
|
||||||
|
if let Some(call_id) = call_id
|
||||||
|
&& let Some(ports) = DIRECT_PLAYER_PORTS.get()
|
||||||
|
{
|
||||||
|
let mut ports = ports.lock();
|
||||||
|
if let Some(call_ports) = ports.get_mut(&call_id) {
|
||||||
|
call_ports.remove(&port_key);
|
||||||
|
if call_ports.is_empty() {
|
||||||
|
ports.remove(&call_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
tracing::debug!("Direct player port destroyed: {:p}", this_port);
|
tracing::debug!("Direct player port destroyed: {:p}", this_port);
|
||||||
}
|
}
|
||||||
pj_constants__PJ_SUCCESS as pj_status_t
|
pj_constants__PJ_SUCCESS as pj_status_t
|
||||||
|
|
@ -101,6 +115,43 @@ pub fn play_audio_to_call_direct(call_id: CallId, samples: &[i16]) -> Result<(),
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stop direct one-shot audio currently playing to a call.
|
||||||
|
pub fn stop_direct_audio_to_call(call_id: CallId) {
|
||||||
|
use super::types::{PendingPjsuaOp, queue_pjsua_op};
|
||||||
|
|
||||||
|
queue_pjsua_op(PendingPjsuaOp::StopDirect { call_id });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal implementation of direct audio stop, run on the audio thread.
|
||||||
|
pub fn stop_direct_audio_to_call_internal(call_id: CallId) {
|
||||||
|
let port_keys = DIRECT_PLAYER_PORTS
|
||||||
|
.get()
|
||||||
|
.and_then(|ports| ports.lock().remove(&call_id));
|
||||||
|
|
||||||
|
let Some(port_keys) = port_keys else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(state) = DIRECT_PLAYER_STATE.get() {
|
||||||
|
let mut state = state.lock();
|
||||||
|
for port_key in &port_keys {
|
||||||
|
state.remove(port_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(calls) = DIRECT_PLAYER_CALLS.get() {
|
||||||
|
let mut calls = calls.lock();
|
||||||
|
for port_key in &port_keys {
|
||||||
|
calls.remove(port_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"Stopped {} direct player(s) for call {}",
|
||||||
|
port_keys.len(),
|
||||||
|
call_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Internal implementation of play_audio_to_call_direct
|
/// Internal implementation of play_audio_to_call_direct
|
||||||
/// Called from the audio thread to actually create and connect the player
|
/// Called from the audio thread to actually create and connect the player
|
||||||
pub fn play_audio_to_call_direct_internal(
|
pub fn play_audio_to_call_direct_internal(
|
||||||
|
|
@ -141,6 +192,14 @@ pub fn play_audio_to_call_direct_internal(
|
||||||
// Now store samples with the actual port key
|
// Now store samples with the actual port key
|
||||||
let state = DIRECT_PLAYER_STATE.get_or_init(|| Mutex::new(HashMap::new()));
|
let state = DIRECT_PLAYER_STATE.get_or_init(|| Mutex::new(HashMap::new()));
|
||||||
state.lock().insert(guard.port_key, (samples.to_vec(), 0));
|
state.lock().insert(guard.port_key, (samples.to_vec(), 0));
|
||||||
|
let ports = DIRECT_PLAYER_PORTS.get_or_init(|| Mutex::new(HashMap::new()));
|
||||||
|
ports
|
||||||
|
.lock()
|
||||||
|
.entry(call_id)
|
||||||
|
.or_insert_with(HashSet::new)
|
||||||
|
.insert(guard.port_key);
|
||||||
|
let calls = DIRECT_PLAYER_CALLS.get_or_init(|| Mutex::new(HashMap::new()));
|
||||||
|
calls.lock().insert(guard.port_key, call_id);
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Playing {} samples directly to call {} (player_slot={}, call_port={})",
|
"Playing {} samples directly to call {} (player_slot={}, call_port={})",
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,8 @@ pub enum PendingPjsuaOp {
|
||||||
/// Play samples directly to a call (for join sounds)
|
/// Play samples directly to a call (for join sounds)
|
||||||
/// Note: This also stops any active looping player for the call first
|
/// Note: This also stops any active looping player for the call first
|
||||||
PlayDirect { call_id: CallId, samples: Vec<i16> },
|
PlayDirect { call_id: CallId, samples: Vec<i16> },
|
||||||
|
/// Stop one-shot direct players for a call.
|
||||||
|
StopDirect { call_id: CallId },
|
||||||
/// Start streaming audio from a file to a call (for large easter egg files)
|
/// Start streaming audio from a file to a call (for large easter egg files)
|
||||||
/// Uses pull model for precise timing - audio thread pulls frames as needed
|
/// Uses pull model for precise timing - audio thread pulls frames as needed
|
||||||
StartStreaming {
|
StartStreaming {
|
||||||
|
|
@ -378,6 +380,13 @@ pub static CHANNEL_DRAIN_CACHE: OnceLock<DashMap<Snowflake, DrainCacheEntry>> =
|
||||||
pub static DIRECT_PLAYER_STATE: OnceLock<Mutex<HashMap<usize, DirectPlayerEntry>>> =
|
pub static DIRECT_PLAYER_STATE: OnceLock<Mutex<HashMap<usize, DirectPlayerEntry>>> =
|
||||||
OnceLock::new();
|
OnceLock::new();
|
||||||
|
|
||||||
|
/// call_id -> direct player port keys.
|
||||||
|
pub static DIRECT_PLAYER_PORTS: OnceLock<Mutex<HashMap<CallId, HashSet<usize>>>> =
|
||||||
|
OnceLock::new();
|
||||||
|
|
||||||
|
/// direct player port key -> call_id.
|
||||||
|
pub static DIRECT_PLAYER_CALLS: OnceLock<Mutex<HashMap<usize, CallId>>> = OnceLock::new();
|
||||||
|
|
||||||
/// Memory pool for direct player ports
|
/// Memory pool for direct player ports
|
||||||
pub static DIRECT_PLAYER_POOL: OnceLock<Mutex<SendablePool>> = OnceLock::new();
|
pub static DIRECT_PLAYER_POOL: OnceLock<Mutex<SendablePool>> = OnceLock::new();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,8 @@ pub enum SipCommand {
|
||||||
/// Play audio directly to a call (bypasses channel buffer)
|
/// Play audio directly to a call (bypasses channel buffer)
|
||||||
/// Used for join sounds to avoid buffer overflow with Discord audio
|
/// Used for join sounds to avoid buffer overflow with Discord audio
|
||||||
PlayDirectToCall { call_id: CallId, samples: Vec<i16> },
|
PlayDirectToCall { call_id: CallId, samples: Vec<i16> },
|
||||||
|
/// Stop one-shot direct audio currently playing to a call.
|
||||||
|
StopDirectToCall { call_id: CallId },
|
||||||
/// Start a looping audio player for early media (183 Session Progress)
|
/// Start a looping audio player for early media (183 Session Progress)
|
||||||
StartConnectingLoop { call_id: CallId, samples: Vec<i16> },
|
StartConnectingLoop { call_id: CallId, samples: Vec<i16> },
|
||||||
/// Hangup a call
|
/// Hangup a call
|
||||||
|
|
@ -364,6 +366,9 @@ fn process_sip_command(cmd: SipCommand, calls: &Arc<DashMap<CallId, CallState>>)
|
||||||
tracing::error!("Failed to play direct audio to call {}: {}", call_id, e);
|
tracing::error!("Failed to play direct audio to call {}: {}", call_id, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SipCommand::StopDirectToCall { call_id } => {
|
||||||
|
stop_direct_audio_to_call(call_id);
|
||||||
|
}
|
||||||
SipCommand::StartConnectingLoop { call_id, samples } => {
|
SipCommand::StartConnectingLoop { call_id, samples } => {
|
||||||
// Queue to audio thread to avoid race with pjmedia_port_get_frame
|
// Queue to audio thread to avoid race with pjmedia_port_get_frame
|
||||||
queue_pjsua_op(PendingPjsuaOp::StartLoop { call_id, samples });
|
queue_pjsua_op(PendingPjsuaOp::StartLoop { call_id, samples });
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue