use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use serde::Deserialize;
use crate::api::unified::{
Config, MediaMode, MediaSessionControllerConfig, RtpSessionBufferConfig,
RtpTransportBufferConfig,
};
use crate::errors::{Result, SessionError};
const DEFAULT_RECIPE_BOOK: &str = include_str!("../../config/performance-recipes.yaml");
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PerformanceConfig {
pub profile: String,
pub capacity: Option<usize>,
pub signaling_only_rtp_port: Option<u16>,
pub recipe_path: Option<PathBuf>,
}
impl PerformanceConfig {
pub fn profile(profile: impl Into<String>) -> Self {
Self {
profile: profile.into(),
capacity: None,
signaling_only_rtp_port: None,
recipe_path: None,
}
}
pub fn endpoint() -> Self {
Self::profile("endpoint")
}
pub fn pbx_media_server(capacity: usize) -> Self {
Self::profile("pbx-media-server").with_capacity(capacity)
}
pub fn signaling_only_server_high_performance(capacity: usize) -> Self {
Self::profile("signaling-only-server-high-performance")
.with_capacity(capacity)
.with_signaling_only_rtp_port(9)
}
pub fn with_capacity(mut self, capacity: usize) -> Self {
self.capacity = Some(capacity);
self
}
pub fn with_signaling_only_rtp_port(mut self, port: u16) -> Self {
self.signaling_only_rtp_port = Some(port);
self
}
pub fn with_recipe_path(mut self, path: impl Into<PathBuf>) -> Self {
self.recipe_path = Some(path.into());
self
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PerformanceRecipeBook {
pub version: u32,
pub performance_profiles: BTreeMap<String, PerformanceRecipe>,
}
impl PerformanceRecipeBook {
pub fn bundled() -> Result<Self> {
Self::from_yaml_str(DEFAULT_RECIPE_BOOK, "bundled default performance recipes")
}
pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let text = fs::read_to_string(path).map_err(|e| {
SessionError::ConfigError(format!(
"failed to read performance recipe file '{}': {e}",
path.display()
))
})?;
Self::from_yaml_str(&text, &path.display().to_string())
}
pub fn from_yaml_str(text: &str, source: &str) -> Result<Self> {
let book: Self = serde_yaml::from_str(text).map_err(|e| {
SessionError::ConfigError(format!(
"failed to parse performance recipe book {source}: {e}"
))
})?;
if book.version != 1 {
return Err(SessionError::ConfigError(format!(
"unsupported performance recipe book version {}; expected 1",
book.version
)));
}
Ok(book)
}
pub fn apply(&self, config: Config, performance: &PerformanceConfig) -> Result<Config> {
let recipe = self
.performance_profiles
.get(&performance.profile)
.ok_or_else(|| {
let mut names = self
.performance_profiles
.keys()
.cloned()
.collect::<Vec<_>>();
names.sort();
SessionError::ConfigError(format!(
"unknown performance profile '{}'; available profiles: {}",
performance.profile,
names.join(", ")
))
})?;
recipe.apply(config, performance)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PerformanceRecipe {
pub description: Option<String>,
pub requires_capacity: Option<bool>,
pub config: PerformanceRecipeConfig,
}
impl PerformanceRecipe {
fn apply(&self, mut config: Config, performance: &PerformanceConfig) -> Result<Config> {
if self.requires_capacity.unwrap_or(false) {
let capacity = performance.capacity.ok_or_else(|| {
SessionError::ConfigError(format!(
"performance profile '{}' requires capacity",
performance.profile
))
})?;
if capacity == 0 {
return Err(SessionError::ConfigError(
"performance capacity must be at least 1".to_string(),
));
}
}
let params = RecipeParams {
capacity: performance.capacity,
signaling_only_rtp_port: performance.signaling_only_rtp_port,
};
self.config.apply(&mut config, ¶ms)?;
config.validate()?;
Ok(config)
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PerformanceRecipeConfig {
pub channel_capacity: Option<RecipeUsize>,
pub auto_180_ringing: Option<bool>,
pub auto_100_trying: Option<bool>,
pub fast_auto_accept_incoming_calls: Option<bool>,
pub sip_udp_diagnostics: Option<bool>,
pub sip_transaction_timing_diagnostics: Option<bool>,
pub sip_dialog_timing_diagnostics: Option<bool>,
pub media_setup_diagnostics: Option<bool>,
pub cleanup_diagnostics: Option<bool>,
pub cleanup_diagnostic_events: Option<bool>,
pub active_call_no_media_timeout_secs: Option<u64>,
pub active_call_media_idle_timeout_secs: Option<u64>,
pub sip_udp_recv_buffer_size: Option<usize>,
pub sip_udp_send_buffer_size: Option<usize>,
pub sip_udp_parse_workers: Option<usize>,
pub sip_udp_parse_queue_capacity: Option<RecipeUsize>,
pub sip_udp_parse_dispatch: Option<RecipeUdpParseDispatch>,
pub sip_transport_dispatch_workers: Option<usize>,
pub sip_transport_dispatch_queue_capacity: Option<RecipeUsize>,
pub sip_transaction_dispatch_workers: Option<usize>,
pub sip_transaction_dispatch_queue_capacity: Option<RecipeUsize>,
pub sip_transaction_dispatch_priority_burst_max: Option<usize>,
pub sip_invite_2xx_retransmit_max_due_per_tick: Option<usize>,
pub sip_transaction_command_channel_capacity: Option<usize>,
pub sip_dialog_dispatch_workers: Option<usize>,
pub sip_dialog_dispatch_queue_capacity: Option<RecipeUsize>,
pub session_event_dispatcher_workers: Option<usize>,
pub session_event_dispatcher_channel_capacity: Option<RecipeUsize>,
pub media_mode: Option<RecipeMediaMode>,
pub signaling_only_rtp_port: Option<RecipeU16>,
pub media_port_capacity: Option<RecipeMediaPortCapacity>,
pub media_session_capacity: Option<RecipeUsize>,
pub rtp_session_buffer_config: Option<RecipeRtpSessionBufferConfig>,
pub rtp_transport_buffer_config: Option<RecipeRtpTransportBufferConfig>,
pub media_session_controller_config: Option<RecipeMediaSessionControllerConfig>,
pub server_call_capacity: Option<RecipeUsize>,
pub server_call_admission_limit: Option<RecipeUsize>,
pub server_call_admission_soft_limit: Option<RecipeUsize>,
pub server_call_admission_pacing_delay_ms: Option<u64>,
pub server_overload_retry_after_secs: Option<u32>,
}
impl PerformanceRecipeConfig {
fn apply(&self, config: &mut Config, params: &RecipeParams) -> Result<()> {
if let Some(capacity) = &self.channel_capacity {
*config = config
.clone()
.with_channel_capacity(capacity.resolve(params, "channelCapacity")?);
}
if let Some(enabled) = self.auto_180_ringing {
config.auto_180_ringing = enabled;
}
if let Some(enabled) = self.auto_100_trying {
config.auto_100_trying = enabled;
}
if let Some(enabled) = self.fast_auto_accept_incoming_calls {
config.fast_auto_accept_incoming_calls = enabled;
}
if let Some(enabled) = self.sip_udp_diagnostics {
config.sip_udp_diagnostics = enabled;
}
if let Some(enabled) = self.sip_transaction_timing_diagnostics {
config.sip_transaction_timing_diagnostics = enabled;
}
if let Some(enabled) = self.sip_dialog_timing_diagnostics {
config.sip_dialog_timing_diagnostics = enabled;
}
if let Some(enabled) = self.media_setup_diagnostics {
config.media_setup_diagnostics = enabled;
}
if let Some(enabled) = self.cleanup_diagnostics {
config.cleanup_diagnostics = enabled;
}
if let Some(enabled) = self.cleanup_diagnostic_events {
config.cleanup_diagnostic_events = enabled;
}
if let Some(seconds) = self.active_call_no_media_timeout_secs {
config.active_call_no_media_timeout_secs = seconds;
}
if let Some(seconds) = self.active_call_media_idle_timeout_secs {
config.active_call_media_idle_timeout_secs = seconds;
}
if let Some(size) = self.sip_udp_recv_buffer_size {
config.sip_udp_recv_buffer_size = Some(size);
}
if let Some(size) = self.sip_udp_send_buffer_size {
config.sip_udp_send_buffer_size = Some(size);
}
if let Some(workers) = self.sip_udp_parse_workers {
config.sip_udp_parse_workers = Some(workers);
}
if let Some(capacity) = &self.sip_udp_parse_queue_capacity {
config.sip_udp_parse_queue_capacity =
Some(capacity.resolve(params, "sipUdpParseQueueCapacity")?);
}
if let Some(dispatch) = self.sip_udp_parse_dispatch {
config.sip_udp_parse_dispatch = Some(dispatch.into());
}
if let Some(workers) = self.sip_transport_dispatch_workers {
config.sip_transport_dispatch_workers = Some(workers);
}
if let Some(capacity) = &self.sip_transport_dispatch_queue_capacity {
config.sip_transport_dispatch_queue_capacity =
Some(capacity.resolve(params, "sipTransportDispatchQueueCapacity")?);
}
if let Some(workers) = self.sip_transaction_dispatch_workers {
config.sip_transaction_dispatch_workers = Some(workers);
}
if let Some(capacity) = &self.sip_transaction_dispatch_queue_capacity {
config.sip_transaction_dispatch_queue_capacity =
Some(capacity.resolve(params, "sipTransactionDispatchQueueCapacity")?);
}
if let Some(max_burst) = self.sip_transaction_dispatch_priority_burst_max {
config.sip_transaction_dispatch_priority_burst_max = Some(max_burst);
}
if let Some(max_due_per_tick) = self.sip_invite_2xx_retransmit_max_due_per_tick {
config.sip_invite_2xx_retransmit_max_due_per_tick = Some(max_due_per_tick);
}
if let Some(capacity) = self.sip_transaction_command_channel_capacity {
config.sip_transaction_command_channel_capacity = Some(capacity);
}
if let Some(workers) = self.sip_dialog_dispatch_workers {
config.sip_dialog_dispatch_workers = Some(workers);
}
if let Some(capacity) = &self.sip_dialog_dispatch_queue_capacity {
config.sip_dialog_dispatch_queue_capacity =
Some(capacity.resolve(params, "sipDialogDispatchQueueCapacity")?);
}
if let Some(workers) = self.session_event_dispatcher_workers {
config.session_event_dispatcher_workers = workers;
}
if let Some(capacity) = &self.session_event_dispatcher_channel_capacity {
config.session_event_dispatcher_channel_capacity =
capacity.resolve(params, "sessionEventDispatcherChannelCapacity")?;
}
if let Some(capacity) = &self.server_call_capacity {
config.server_call_capacity = Some(capacity.resolve(params, "serverCallCapacity")?);
}
if let Some(limit) = &self.server_call_admission_limit {
config.server_call_admission_limit =
Some(limit.resolve(params, "serverCallAdmissionLimit")?);
}
if let Some(limit) = &self.server_call_admission_soft_limit {
config.server_call_admission_soft_limit =
Some(limit.resolve(params, "serverCallAdmissionSoftLimit")?);
}
if let Some(delay_ms) = self.server_call_admission_pacing_delay_ms {
config.server_call_admission_pacing_delay_ms = Some(delay_ms);
}
if let Some(seconds) = self.server_overload_retry_after_secs {
config.server_overload_retry_after_secs = Some(seconds);
}
if let Some(port_capacity) = &self.media_port_capacity {
*config = config.clone().with_media_port_capacity(
port_capacity.start,
port_capacity
.capacity
.resolve(params, "mediaPortCapacity.capacity")?,
);
}
if let Some(capacity) = &self.media_session_capacity {
config.media_session_capacity = Some(capacity.resolve(params, "mediaSessionCapacity")?);
}
if let Some(recipe_config) = &self.media_session_controller_config {
let mut controller_config = config.media_session_controller_config.clone();
recipe_config.apply(&mut controller_config, params)?;
config.media_session_controller_config = controller_config;
config.rtp_session_buffer_config = config
.media_session_controller_config
.rtp_session_buffer_config;
config.rtp_transport_buffer_config = config
.media_session_controller_config
.rtp_transport_buffer_config;
}
if let Some(recipe_config) = &self.rtp_session_buffer_config {
let mut rtp_config = config.rtp_session_buffer_config;
recipe_config.apply(&mut rtp_config, params)?;
config.rtp_session_buffer_config = rtp_config;
config
.media_session_controller_config
.rtp_session_buffer_config = rtp_config;
}
if let Some(recipe_config) = &self.rtp_transport_buffer_config {
let mut rtp_config = config.rtp_transport_buffer_config;
recipe_config.apply(&mut rtp_config, params)?;
config.rtp_transport_buffer_config = rtp_config;
config
.media_session_controller_config
.rtp_transport_buffer_config = rtp_config;
}
if let Some(mode) = self.media_mode {
config.media_mode = match mode {
RecipeMediaMode::Enabled => MediaMode::Enabled,
RecipeMediaMode::SignalingOnly => {
let port = self
.signaling_only_rtp_port
.as_ref()
.map(|p| p.resolve(params, "signalingOnlyRtpPort"))
.transpose()?
.unwrap_or(9);
MediaMode::SignalingOnly { sdp_rtp_port: port }
}
};
}
Ok(())
}
}
struct RecipeParams {
capacity: Option<usize>,
signaling_only_rtp_port: Option<u16>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum RecipeUsize {
Literal(usize),
Variable(String),
}
impl RecipeUsize {
fn resolve(&self, params: &RecipeParams, field: &str) -> Result<usize> {
match self {
Self::Literal(value) => Ok(*value),
Self::Variable(name) if name == "$capacity" || name == "capacity" => {
params.capacity.ok_or_else(|| {
SessionError::ConfigError(format!("{field} requires performance capacity"))
})
}
Self::Variable(name) if name == "$capacity90Percent" || name == "capacity90Percent" => {
params
.capacity
.map(|capacity| capacity.saturating_mul(90).div_ceil(100).max(1))
.ok_or_else(|| {
SessionError::ConfigError(format!("{field} requires performance capacity"))
})
}
Self::Variable(name) => Err(SessionError::ConfigError(format!(
"unsupported variable '{name}' in performance recipe field {field}"
))),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum RecipeU16 {
Literal(u16),
Variable(String),
}
impl RecipeU16 {
fn resolve(&self, params: &RecipeParams, field: &str) -> Result<u16> {
match self {
Self::Literal(value) => Ok(*value),
Self::Variable(name)
if name == "$signalingOnlyRtpPort" || name == "signalingOnlyRtpPort" =>
{
Ok(params.signaling_only_rtp_port.unwrap_or(9))
}
Self::Variable(name) => Err(SessionError::ConfigError(format!(
"unsupported variable '{name}' in performance recipe field {field}"
))),
}
}
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RecipeUdpParseDispatch {
SourceHash,
RoundRobin,
}
impl From<RecipeUdpParseDispatch> for rvoip_sip_transport::UdpParseDispatch {
fn from(value: RecipeUdpParseDispatch) -> Self {
match value {
RecipeUdpParseDispatch::SourceHash => Self::SourceHash,
RecipeUdpParseDispatch::RoundRobin => Self::RoundRobin,
}
}
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RecipeMediaMode {
Enabled,
SignalingOnly,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecipeMediaPortCapacity {
pub start: u16,
pub capacity: RecipeUsize,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecipeRtpSessionBufferConfig {
pub sender_channel_capacity: Option<RecipeUsize>,
pub receiver_channel_capacity: Option<RecipeUsize>,
pub event_channel_capacity: Option<RecipeUsize>,
}
impl RecipeRtpSessionBufferConfig {
fn apply(&self, config: &mut RtpSessionBufferConfig, params: &RecipeParams) -> Result<()> {
if let Some(value) = &self.sender_channel_capacity {
config.sender_channel_capacity =
value.resolve(params, "rtpSessionBufferConfig.senderChannelCapacity")?;
}
if let Some(value) = &self.receiver_channel_capacity {
config.receiver_channel_capacity =
value.resolve(params, "rtpSessionBufferConfig.receiverChannelCapacity")?;
}
if let Some(value) = &self.event_channel_capacity {
config.event_channel_capacity =
value.resolve(params, "rtpSessionBufferConfig.eventChannelCapacity")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecipeRtpTransportBufferConfig {
pub event_channel_capacity: Option<RecipeUsize>,
pub recv_buffer_size: Option<RecipeUsize>,
pub rtcp_recv_buffer_size: Option<RecipeUsize>,
}
impl RecipeRtpTransportBufferConfig {
fn apply(&self, config: &mut RtpTransportBufferConfig, params: &RecipeParams) -> Result<()> {
if let Some(value) = &self.event_channel_capacity {
config.event_channel_capacity =
value.resolve(params, "rtpTransportBufferConfig.eventChannelCapacity")?;
}
if let Some(value) = &self.recv_buffer_size {
config.recv_buffer_size =
value.resolve(params, "rtpTransportBufferConfig.recvBufferSize")?;
}
if let Some(value) = &self.rtcp_recv_buffer_size {
config.rtcp_recv_buffer_size =
value.resolve(params, "rtpTransportBufferConfig.rtcpRecvBufferSize")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecipeMediaSessionControllerConfig {
pub capacity_hint: Option<RecipeUsize>,
pub audio_frame_pool: Option<RecipeAudioFramePoolConfig>,
pub rtp_buffer_size: Option<RecipeUsize>,
pub rtp_buffer_initial_count: Option<RecipeUsize>,
pub rtp_buffer_max_count: Option<RecipeUsize>,
pub rtp_session_buffer_config: Option<RecipeRtpSessionBufferConfig>,
pub rtp_transport_buffer_config: Option<RecipeRtpTransportBufferConfig>,
}
impl RecipeMediaSessionControllerConfig {
fn apply(
&self,
config: &mut MediaSessionControllerConfig,
params: &RecipeParams,
) -> Result<()> {
if let Some(value) = &self.capacity_hint {
config.capacity_hint =
value.resolve(params, "mediaSessionControllerConfig.capacityHint")?;
}
if let Some(pool) = &self.audio_frame_pool {
pool.apply(config, params)?;
}
if let Some(value) = &self.rtp_buffer_size {
config.rtp_buffer_size =
value.resolve(params, "mediaSessionControllerConfig.rtpBufferSize")?;
}
if let Some(value) = &self.rtp_buffer_initial_count {
config.rtp_buffer_initial_count =
value.resolve(params, "mediaSessionControllerConfig.rtpBufferInitialCount")?;
}
if let Some(value) = &self.rtp_buffer_max_count {
config.rtp_buffer_max_count =
value.resolve(params, "mediaSessionControllerConfig.rtpBufferMaxCount")?;
}
if let Some(rtp_config) = &self.rtp_session_buffer_config {
rtp_config.apply(&mut config.rtp_session_buffer_config, params)?;
}
if let Some(rtp_config) = &self.rtp_transport_buffer_config {
rtp_config.apply(&mut config.rtp_transport_buffer_config, params)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecipeAudioFramePoolConfig {
pub initial_size: Option<RecipeUsize>,
pub max_size: Option<RecipeUsize>,
pub sample_rate: Option<u32>,
pub channels: Option<u8>,
pub samples_per_frame: Option<RecipeUsize>,
}
impl RecipeAudioFramePoolConfig {
fn apply(
&self,
config: &mut MediaSessionControllerConfig,
params: &RecipeParams,
) -> Result<()> {
if let Some(value) = &self.initial_size {
config.audio_frame_pool.initial_size = value.resolve(
params,
"mediaSessionControllerConfig.audioFramePool.initialSize",
)?;
}
if let Some(value) = &self.max_size {
config.audio_frame_pool.max_size = value.resolve(
params,
"mediaSessionControllerConfig.audioFramePool.maxSize",
)?;
}
if let Some(value) = self.sample_rate {
config.audio_frame_pool.sample_rate = value;
}
if let Some(value) = self.channels {
config.audio_frame_pool.channels = value;
}
if let Some(value) = &self.samples_per_frame {
config.audio_frame_pool.samples_per_frame = value.resolve(
params,
"mediaSessionControllerConfig.audioFramePool.samplesPerFrame",
)?;
}
Ok(())
}
}