Skip to main content

rvoip_sip/api/
performance.rs

1//! Config-backed performance recipe loading and application.
2//!
3//! The recipe values live in YAML so deployments and release gates can tune
4//! server shapes without changing library source. The library owns parsing,
5//! validation, and application to [`Config`].
6
7use std::collections::BTreeMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11use serde::Deserialize;
12
13use crate::api::unified::{
14    Config, MediaMode, MediaSessionControllerConfig, RtpSessionBufferConfig,
15    RtpTransportBufferConfig,
16};
17use crate::errors::{Result, SessionError};
18
19const DEFAULT_RECIPE_BOOK: &str = include_str!("../../config/performance-recipes.yaml");
20
21/// Parameterized performance recipe selection.
22///
23/// `profile` is resolved from a YAML recipe book. When `recipe_path` is
24/// omitted, rvoip-sip uses its bundled default recipe book. When `recipe_path`
25/// is set, that YAML file is loaded instead.
26#[derive(Debug, Clone, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct PerformanceConfig {
29    /// Recipe name from the selected YAML recipe book.
30    pub profile: String,
31    /// Expected burst or active-call capacity for capacity-driven recipes.
32    pub capacity: Option<usize>,
33    /// SDP RTP port used by signaling-only recipes.
34    pub signaling_only_rtp_port: Option<u16>,
35    /// Optional YAML recipe book path.
36    pub recipe_path: Option<PathBuf>,
37}
38
39impl PerformanceConfig {
40    /// Create a performance config for a named recipe profile.
41    pub fn profile(profile: impl Into<String>) -> Self {
42        Self {
43            profile: profile.into(),
44            capacity: None,
45            signaling_only_rtp_port: None,
46            recipe_path: None,
47        }
48    }
49
50    /// Endpoint recipe. This is also the implicit endpoint default.
51    pub fn endpoint() -> Self {
52        Self::profile("endpoint")
53    }
54
55    /// PBX-style media server recipe.
56    pub fn pbx_media_server(capacity: usize) -> Self {
57        Self::profile("pbx-media-server").with_capacity(capacity)
58    }
59
60    /// High-performance signaling-only server recipe.
61    pub fn signaling_only_server_high_performance(capacity: usize) -> Self {
62        Self::profile("signaling-only-server-high-performance")
63            .with_capacity(capacity)
64            .with_signaling_only_rtp_port(9)
65    }
66
67    /// Set the capacity parameter.
68    pub fn with_capacity(mut self, capacity: usize) -> Self {
69        self.capacity = Some(capacity);
70        self
71    }
72
73    /// Set the signaling-only SDP RTP port parameter.
74    pub fn with_signaling_only_rtp_port(mut self, port: u16) -> Self {
75        self.signaling_only_rtp_port = Some(port);
76        self
77    }
78
79    /// Load recipes from this YAML path instead of the bundled default book.
80    pub fn with_recipe_path(mut self, path: impl Into<PathBuf>) -> Self {
81        self.recipe_path = Some(path.into());
82        self
83    }
84}
85
86/// YAML performance recipe book.
87#[derive(Debug, Clone, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct PerformanceRecipeBook {
90    /// Recipe schema version.
91    pub version: u32,
92    /// Named performance profiles.
93    pub performance_profiles: BTreeMap<String, PerformanceRecipe>,
94}
95
96impl PerformanceRecipeBook {
97    /// Parse the bundled default recipe book.
98    pub fn bundled() -> Result<Self> {
99        Self::from_yaml_str(DEFAULT_RECIPE_BOOK, "bundled default performance recipes")
100    }
101
102    /// Load a recipe book from a YAML file.
103    pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
104        let path = path.as_ref();
105        let text = fs::read_to_string(path).map_err(|e| {
106            SessionError::ConfigError(format!(
107                "failed to read performance recipe file '{}': {e}",
108                path.display()
109            ))
110        })?;
111        Self::from_yaml_str(&text, &path.display().to_string())
112    }
113
114    /// Parse a YAML recipe book string.
115    pub fn from_yaml_str(text: &str, source: &str) -> Result<Self> {
116        let book: Self = serde_yaml::from_str(text).map_err(|e| {
117            SessionError::ConfigError(format!(
118                "failed to parse performance recipe book {source}: {e}"
119            ))
120        })?;
121        if book.version != 1 {
122            return Err(SessionError::ConfigError(format!(
123                "unsupported performance recipe book version {}; expected 1",
124                book.version
125            )));
126        }
127        Ok(book)
128    }
129
130    /// Apply a named performance config to an existing [`Config`].
131    pub fn apply(&self, config: Config, performance: &PerformanceConfig) -> Result<Config> {
132        let recipe = self
133            .performance_profiles
134            .get(&performance.profile)
135            .ok_or_else(|| {
136                let mut names = self
137                    .performance_profiles
138                    .keys()
139                    .cloned()
140                    .collect::<Vec<_>>();
141                names.sort();
142                SessionError::ConfigError(format!(
143                    "unknown performance profile '{}'; available profiles: {}",
144                    performance.profile,
145                    names.join(", ")
146                ))
147            })?;
148        recipe.apply(config, performance)
149    }
150}
151
152/// One named performance recipe from YAML.
153#[derive(Debug, Clone, Deserialize)]
154#[serde(rename_all = "camelCase")]
155pub struct PerformanceRecipe {
156    /// Human-readable recipe description.
157    pub description: Option<String>,
158    /// Whether `PerformanceConfig.capacity` is required.
159    pub requires_capacity: Option<bool>,
160    /// Config field values to apply.
161    pub config: PerformanceRecipeConfig,
162}
163
164impl PerformanceRecipe {
165    fn apply(&self, mut config: Config, performance: &PerformanceConfig) -> Result<Config> {
166        if self.requires_capacity.unwrap_or(false) {
167            let capacity = performance.capacity.ok_or_else(|| {
168                SessionError::ConfigError(format!(
169                    "performance profile '{}' requires capacity",
170                    performance.profile
171                ))
172            })?;
173            if capacity == 0 {
174                return Err(SessionError::ConfigError(
175                    "performance capacity must be at least 1".to_string(),
176                ));
177            }
178        }
179
180        let params = RecipeParams {
181            capacity: performance.capacity,
182            signaling_only_rtp_port: performance.signaling_only_rtp_port,
183        };
184        self.config.apply(&mut config, &params)?;
185        config.validate()?;
186        Ok(config)
187    }
188}
189
190/// Config mutations supported by YAML performance recipes.
191#[derive(Debug, Clone, Default, Deserialize)]
192#[serde(rename_all = "camelCase")]
193pub struct PerformanceRecipeConfig {
194    /// Capacity for the standard SIP signaling queues.
195    pub channel_capacity: Option<RecipeUsize>,
196    /// Whether automatic `180 Ringing` is sent for inbound INVITEs.
197    pub auto_180_ringing: Option<bool>,
198    /// Whether automatic `100 Trying` timer tasks are armed.
199    pub auto_100_trying: Option<bool>,
200    /// Whether inbound INVITEs are accepted before app callbacks.
201    pub fast_auto_accept_incoming_calls: Option<bool>,
202    /// Enable SIP UDP transport and duplicate-recovery diagnostics.
203    pub sip_udp_diagnostics: Option<bool>,
204    /// Enable high-cardinality transaction timing diagnostics.
205    pub sip_transaction_timing_diagnostics: Option<bool>,
206    /// Enable high-cardinality dialog timing diagnostics.
207    pub sip_dialog_timing_diagnostics: Option<bool>,
208    /// Enable media setup/teardown timing diagnostics.
209    pub media_setup_diagnostics: Option<bool>,
210    /// Enable cleanup-stage timing diagnostics.
211    pub cleanup_diagnostics: Option<bool>,
212    /// Enable per-operation cleanup diagnostic log lines.
213    pub cleanup_diagnostic_events: Option<bool>,
214    /// Active inbound no-RTP watchdog timeout in seconds.
215    pub active_call_no_media_timeout_secs: Option<u64>,
216    /// Active inbound RTP idle watchdog timeout in seconds.
217    pub active_call_media_idle_timeout_secs: Option<u64>,
218    /// SIP UDP receive socket buffer size in bytes.
219    pub sip_udp_recv_buffer_size: Option<usize>,
220    /// SIP UDP send socket buffer size in bytes.
221    pub sip_udp_send_buffer_size: Option<usize>,
222    /// UDP parse worker count.
223    pub sip_udp_parse_workers: Option<usize>,
224    /// Per-worker UDP parse queue capacity.
225    pub sip_udp_parse_queue_capacity: Option<RecipeUsize>,
226    /// UDP parse dispatch strategy.
227    pub sip_udp_parse_dispatch: Option<RecipeUdpParseDispatch>,
228    /// Transport-manager event dispatch worker count.
229    pub sip_transport_dispatch_workers: Option<usize>,
230    /// Transport-manager event dispatch queue capacity.
231    pub sip_transport_dispatch_queue_capacity: Option<RecipeUsize>,
232    /// Transaction-manager ingress dispatch worker count.
233    pub sip_transaction_dispatch_workers: Option<usize>,
234    /// Transaction-manager ingress dispatch queue capacity.
235    pub sip_transaction_dispatch_queue_capacity: Option<RecipeUsize>,
236    /// Transaction-manager ACK/BYE priority burst limit.
237    pub sip_transaction_dispatch_priority_burst_max: Option<usize>,
238    /// INVITE 2xx retransmit due-item budget per maintenance tick.
239    pub sip_invite_2xx_retransmit_max_due_per_tick: Option<usize>,
240    /// Per-transaction command channel capacity.
241    pub sip_transaction_command_channel_capacity: Option<usize>,
242    /// Dialog-core transaction-event dispatch worker count.
243    pub sip_dialog_dispatch_workers: Option<usize>,
244    /// Dialog-core transaction-event dispatch queue capacity.
245    pub sip_dialog_dispatch_queue_capacity: Option<RecipeUsize>,
246    /// App-session event dispatcher worker count.
247    pub session_event_dispatcher_workers: Option<usize>,
248    /// Per-worker app-session event dispatcher queue capacity.
249    pub session_event_dispatcher_channel_capacity: Option<RecipeUsize>,
250    /// Media behavior.
251    pub media_mode: Option<RecipeMediaMode>,
252    /// SDP RTP port for signaling-only media mode.
253    pub signaling_only_rtp_port: Option<RecipeU16>,
254    /// RTP media port range by start and requested capacity.
255    pub media_port_capacity: Option<RecipeMediaPortCapacity>,
256    /// Media-core session and RTP allocator capacity hint.
257    pub media_session_capacity: Option<RecipeUsize>,
258    /// RTP session queue sizing.
259    pub rtp_session_buffer_config: Option<RecipeRtpSessionBufferConfig>,
260    /// RTP transport event and receive buffer sizing.
261    pub rtp_transport_buffer_config: Option<RecipeRtpTransportBufferConfig>,
262    /// Media-core controller pool and capacity tuning.
263    pub media_session_controller_config: Option<RecipeMediaSessionControllerConfig>,
264    /// Server-side active call capacity hint.
265    pub server_call_capacity: Option<RecipeUsize>,
266    /// Server-side inbound call admission limit.
267    pub server_call_admission_limit: Option<RecipeUsize>,
268    /// Server-side inbound call admission soft pacing threshold.
269    pub server_call_admission_soft_limit: Option<RecipeUsize>,
270    /// Delay in milliseconds while above the soft admission threshold.
271    pub server_call_admission_pacing_delay_ms: Option<u64>,
272    /// Retry-After seconds for server overload rejections.
273    pub server_overload_retry_after_secs: Option<u32>,
274}
275
276impl PerformanceRecipeConfig {
277    fn apply(&self, config: &mut Config, params: &RecipeParams) -> Result<()> {
278        if let Some(capacity) = &self.channel_capacity {
279            *config = config
280                .clone()
281                .with_channel_capacity(capacity.resolve(params, "channelCapacity")?);
282        }
283        if let Some(enabled) = self.auto_180_ringing {
284            config.auto_180_ringing = enabled;
285        }
286        if let Some(enabled) = self.auto_100_trying {
287            config.auto_100_trying = enabled;
288        }
289        if let Some(enabled) = self.fast_auto_accept_incoming_calls {
290            config.fast_auto_accept_incoming_calls = enabled;
291        }
292        if let Some(enabled) = self.sip_udp_diagnostics {
293            config.sip_udp_diagnostics = enabled;
294        }
295        if let Some(enabled) = self.sip_transaction_timing_diagnostics {
296            config.sip_transaction_timing_diagnostics = enabled;
297        }
298        if let Some(enabled) = self.sip_dialog_timing_diagnostics {
299            config.sip_dialog_timing_diagnostics = enabled;
300        }
301        if let Some(enabled) = self.media_setup_diagnostics {
302            config.media_setup_diagnostics = enabled;
303        }
304        if let Some(enabled) = self.cleanup_diagnostics {
305            config.cleanup_diagnostics = enabled;
306        }
307        if let Some(enabled) = self.cleanup_diagnostic_events {
308            config.cleanup_diagnostic_events = enabled;
309        }
310        if let Some(seconds) = self.active_call_no_media_timeout_secs {
311            config.active_call_no_media_timeout_secs = seconds;
312        }
313        if let Some(seconds) = self.active_call_media_idle_timeout_secs {
314            config.active_call_media_idle_timeout_secs = seconds;
315        }
316        if let Some(size) = self.sip_udp_recv_buffer_size {
317            config.sip_udp_recv_buffer_size = Some(size);
318        }
319        if let Some(size) = self.sip_udp_send_buffer_size {
320            config.sip_udp_send_buffer_size = Some(size);
321        }
322        if let Some(workers) = self.sip_udp_parse_workers {
323            config.sip_udp_parse_workers = Some(workers);
324        }
325        if let Some(capacity) = &self.sip_udp_parse_queue_capacity {
326            config.sip_udp_parse_queue_capacity =
327                Some(capacity.resolve(params, "sipUdpParseQueueCapacity")?);
328        }
329        if let Some(dispatch) = self.sip_udp_parse_dispatch {
330            config.sip_udp_parse_dispatch = Some(dispatch.into());
331        }
332        if let Some(workers) = self.sip_transport_dispatch_workers {
333            config.sip_transport_dispatch_workers = Some(workers);
334        }
335        if let Some(capacity) = &self.sip_transport_dispatch_queue_capacity {
336            config.sip_transport_dispatch_queue_capacity =
337                Some(capacity.resolve(params, "sipTransportDispatchQueueCapacity")?);
338        }
339        if let Some(workers) = self.sip_transaction_dispatch_workers {
340            config.sip_transaction_dispatch_workers = Some(workers);
341        }
342        if let Some(capacity) = &self.sip_transaction_dispatch_queue_capacity {
343            config.sip_transaction_dispatch_queue_capacity =
344                Some(capacity.resolve(params, "sipTransactionDispatchQueueCapacity")?);
345        }
346        if let Some(max_burst) = self.sip_transaction_dispatch_priority_burst_max {
347            config.sip_transaction_dispatch_priority_burst_max = Some(max_burst);
348        }
349        if let Some(max_due_per_tick) = self.sip_invite_2xx_retransmit_max_due_per_tick {
350            config.sip_invite_2xx_retransmit_max_due_per_tick = Some(max_due_per_tick);
351        }
352        if let Some(capacity) = self.sip_transaction_command_channel_capacity {
353            config.sip_transaction_command_channel_capacity = Some(capacity);
354        }
355        if let Some(workers) = self.sip_dialog_dispatch_workers {
356            config.sip_dialog_dispatch_workers = Some(workers);
357        }
358        if let Some(capacity) = &self.sip_dialog_dispatch_queue_capacity {
359            config.sip_dialog_dispatch_queue_capacity =
360                Some(capacity.resolve(params, "sipDialogDispatchQueueCapacity")?);
361        }
362        if let Some(workers) = self.session_event_dispatcher_workers {
363            config.session_event_dispatcher_workers = workers;
364        }
365        if let Some(capacity) = &self.session_event_dispatcher_channel_capacity {
366            config.session_event_dispatcher_channel_capacity =
367                capacity.resolve(params, "sessionEventDispatcherChannelCapacity")?;
368        }
369        if let Some(capacity) = &self.server_call_capacity {
370            config.server_call_capacity = Some(capacity.resolve(params, "serverCallCapacity")?);
371        }
372        if let Some(limit) = &self.server_call_admission_limit {
373            config.server_call_admission_limit =
374                Some(limit.resolve(params, "serverCallAdmissionLimit")?);
375        }
376        if let Some(limit) = &self.server_call_admission_soft_limit {
377            config.server_call_admission_soft_limit =
378                Some(limit.resolve(params, "serverCallAdmissionSoftLimit")?);
379        }
380        if let Some(delay_ms) = self.server_call_admission_pacing_delay_ms {
381            config.server_call_admission_pacing_delay_ms = Some(delay_ms);
382        }
383        if let Some(seconds) = self.server_overload_retry_after_secs {
384            config.server_overload_retry_after_secs = Some(seconds);
385        }
386        if let Some(port_capacity) = &self.media_port_capacity {
387            *config = config.clone().with_media_port_capacity(
388                port_capacity.start,
389                port_capacity
390                    .capacity
391                    .resolve(params, "mediaPortCapacity.capacity")?,
392            );
393        }
394        if let Some(capacity) = &self.media_session_capacity {
395            config.media_session_capacity = Some(capacity.resolve(params, "mediaSessionCapacity")?);
396        }
397        if let Some(recipe_config) = &self.media_session_controller_config {
398            let mut controller_config = config.media_session_controller_config.clone();
399            recipe_config.apply(&mut controller_config, params)?;
400            config.media_session_controller_config = controller_config;
401            config.rtp_session_buffer_config = config
402                .media_session_controller_config
403                .rtp_session_buffer_config;
404            config.rtp_transport_buffer_config = config
405                .media_session_controller_config
406                .rtp_transport_buffer_config;
407        }
408        if let Some(recipe_config) = &self.rtp_session_buffer_config {
409            let mut rtp_config = config.rtp_session_buffer_config;
410            recipe_config.apply(&mut rtp_config, params)?;
411            config.rtp_session_buffer_config = rtp_config;
412            config
413                .media_session_controller_config
414                .rtp_session_buffer_config = rtp_config;
415        }
416        if let Some(recipe_config) = &self.rtp_transport_buffer_config {
417            let mut rtp_config = config.rtp_transport_buffer_config;
418            recipe_config.apply(&mut rtp_config, params)?;
419            config.rtp_transport_buffer_config = rtp_config;
420            config
421                .media_session_controller_config
422                .rtp_transport_buffer_config = rtp_config;
423        }
424        if let Some(mode) = self.media_mode {
425            config.media_mode = match mode {
426                RecipeMediaMode::Enabled => MediaMode::Enabled,
427                RecipeMediaMode::SignalingOnly => {
428                    let port = self
429                        .signaling_only_rtp_port
430                        .as_ref()
431                        .map(|p| p.resolve(params, "signalingOnlyRtpPort"))
432                        .transpose()?
433                        .unwrap_or(9);
434                    MediaMode::SignalingOnly { sdp_rtp_port: port }
435                }
436            };
437        }
438        Ok(())
439    }
440}
441
442struct RecipeParams {
443    capacity: Option<usize>,
444    signaling_only_rtp_port: Option<u16>,
445}
446
447/// Recipe value that can be a literal integer or `$capacity`.
448#[derive(Debug, Clone, Deserialize)]
449#[serde(untagged)]
450pub enum RecipeUsize {
451    /// Literal value.
452    Literal(usize),
453    /// Variable reference.
454    Variable(String),
455}
456
457impl RecipeUsize {
458    fn resolve(&self, params: &RecipeParams, field: &str) -> Result<usize> {
459        match self {
460            Self::Literal(value) => Ok(*value),
461            Self::Variable(name) if name == "$capacity" || name == "capacity" => {
462                params.capacity.ok_or_else(|| {
463                    SessionError::ConfigError(format!("{field} requires performance capacity"))
464                })
465            }
466            Self::Variable(name) if name == "$capacity90Percent" || name == "capacity90Percent" => {
467                params
468                    .capacity
469                    .map(|capacity| capacity.saturating_mul(90).div_ceil(100).max(1))
470                    .ok_or_else(|| {
471                        SessionError::ConfigError(format!("{field} requires performance capacity"))
472                    })
473            }
474            Self::Variable(name) => Err(SessionError::ConfigError(format!(
475                "unsupported variable '{name}' in performance recipe field {field}"
476            ))),
477        }
478    }
479}
480
481/// Recipe value that can be a literal port or `$signalingOnlyRtpPort`.
482#[derive(Debug, Clone, Deserialize)]
483#[serde(untagged)]
484pub enum RecipeU16 {
485    /// Literal value.
486    Literal(u16),
487    /// Variable reference.
488    Variable(String),
489}
490
491impl RecipeU16 {
492    fn resolve(&self, params: &RecipeParams, field: &str) -> Result<u16> {
493        match self {
494            Self::Literal(value) => Ok(*value),
495            Self::Variable(name)
496                if name == "$signalingOnlyRtpPort" || name == "signalingOnlyRtpPort" =>
497            {
498                Ok(params.signaling_only_rtp_port.unwrap_or(9))
499            }
500            Self::Variable(name) => Err(SessionError::ConfigError(format!(
501                "unsupported variable '{name}' in performance recipe field {field}"
502            ))),
503        }
504    }
505}
506
507/// UDP parse dispatch strategy in recipe YAML.
508#[derive(Debug, Clone, Copy, Deserialize)]
509#[serde(rename_all = "kebab-case")]
510pub enum RecipeUdpParseDispatch {
511    /// Preserve per-source ordering.
512    SourceHash,
513    /// Spread received datagrams across workers.
514    RoundRobin,
515}
516
517impl From<RecipeUdpParseDispatch> for rvoip_sip_transport::UdpParseDispatch {
518    fn from(value: RecipeUdpParseDispatch) -> Self {
519        match value {
520            RecipeUdpParseDispatch::SourceHash => Self::SourceHash,
521            RecipeUdpParseDispatch::RoundRobin => Self::RoundRobin,
522        }
523    }
524}
525
526/// Media mode in recipe YAML.
527#[derive(Debug, Clone, Copy, Deserialize)]
528#[serde(rename_all = "kebab-case")]
529pub enum RecipeMediaMode {
530    /// Use real media-core RTP allocation.
531    Enabled,
532    /// Generate SDP without media-core RTP allocation.
533    SignalingOnly,
534}
535
536/// Media port capacity recipe value.
537#[derive(Debug, Clone, Deserialize)]
538#[serde(rename_all = "camelCase")]
539pub struct RecipeMediaPortCapacity {
540    /// RTP media port range start.
541    pub start: u16,
542    /// Requested number of ports.
543    pub capacity: RecipeUsize,
544}
545
546/// RTP session buffer config recipe values.
547#[derive(Debug, Clone, Default, Deserialize)]
548#[serde(rename_all = "camelCase")]
549pub struct RecipeRtpSessionBufferConfig {
550    /// Bounded sender queue capacity in RTP packets.
551    pub sender_channel_capacity: Option<RecipeUsize>,
552    /// Bounded legacy polling receive queue capacity in RTP packets.
553    pub receiver_channel_capacity: Option<RecipeUsize>,
554    /// Broadcast ring capacity for RTP session events.
555    pub event_channel_capacity: Option<RecipeUsize>,
556}
557
558impl RecipeRtpSessionBufferConfig {
559    fn apply(&self, config: &mut RtpSessionBufferConfig, params: &RecipeParams) -> Result<()> {
560        if let Some(value) = &self.sender_channel_capacity {
561            config.sender_channel_capacity =
562                value.resolve(params, "rtpSessionBufferConfig.senderChannelCapacity")?;
563        }
564        if let Some(value) = &self.receiver_channel_capacity {
565            config.receiver_channel_capacity =
566                value.resolve(params, "rtpSessionBufferConfig.receiverChannelCapacity")?;
567        }
568        if let Some(value) = &self.event_channel_capacity {
569            config.event_channel_capacity =
570                value.resolve(params, "rtpSessionBufferConfig.eventChannelCapacity")?;
571        }
572        Ok(())
573    }
574}
575
576/// RTP transport buffer config recipe values.
577#[derive(Debug, Clone, Default, Deserialize)]
578#[serde(rename_all = "camelCase")]
579pub struct RecipeRtpTransportBufferConfig {
580    /// Broadcast ring capacity for RTP/RTCP transport events.
581    pub event_channel_capacity: Option<RecipeUsize>,
582    /// UDP RTP receive buffer size in bytes.
583    pub recv_buffer_size: Option<RecipeUsize>,
584    /// UDP RTCP receive buffer size in bytes for separate RTCP sockets.
585    pub rtcp_recv_buffer_size: Option<RecipeUsize>,
586}
587
588impl RecipeRtpTransportBufferConfig {
589    fn apply(&self, config: &mut RtpTransportBufferConfig, params: &RecipeParams) -> Result<()> {
590        if let Some(value) = &self.event_channel_capacity {
591            config.event_channel_capacity =
592                value.resolve(params, "rtpTransportBufferConfig.eventChannelCapacity")?;
593        }
594        if let Some(value) = &self.recv_buffer_size {
595            config.recv_buffer_size =
596                value.resolve(params, "rtpTransportBufferConfig.recvBufferSize")?;
597        }
598        if let Some(value) = &self.rtcp_recv_buffer_size {
599            config.rtcp_recv_buffer_size =
600                value.resolve(params, "rtpTransportBufferConfig.rtcpRecvBufferSize")?;
601        }
602        Ok(())
603    }
604}
605
606/// Media controller buffer and pool config recipe values.
607#[derive(Debug, Clone, Default, Deserialize)]
608#[serde(rename_all = "camelCase")]
609pub struct RecipeMediaSessionControllerConfig {
610    /// Initial capacity for hot session indexes.
611    pub capacity_hint: Option<RecipeUsize>,
612    /// Shared audio frame pool configuration.
613    pub audio_frame_pool: Option<RecipeAudioFramePoolConfig>,
614    /// RTP output buffer size in bytes.
615    pub rtp_buffer_size: Option<RecipeUsize>,
616    /// Initial RTP output buffer count.
617    pub rtp_buffer_initial_count: Option<RecipeUsize>,
618    /// Maximum RTP output buffer count.
619    pub rtp_buffer_max_count: Option<RecipeUsize>,
620    /// RTP session queue and reusable send-buffer sizing.
621    pub rtp_session_buffer_config: Option<RecipeRtpSessionBufferConfig>,
622    /// RTP transport event and receive buffer sizing.
623    pub rtp_transport_buffer_config: Option<RecipeRtpTransportBufferConfig>,
624}
625
626impl RecipeMediaSessionControllerConfig {
627    fn apply(
628        &self,
629        config: &mut MediaSessionControllerConfig,
630        params: &RecipeParams,
631    ) -> Result<()> {
632        if let Some(value) = &self.capacity_hint {
633            config.capacity_hint =
634                value.resolve(params, "mediaSessionControllerConfig.capacityHint")?;
635        }
636        if let Some(pool) = &self.audio_frame_pool {
637            pool.apply(config, params)?;
638        }
639        if let Some(value) = &self.rtp_buffer_size {
640            config.rtp_buffer_size =
641                value.resolve(params, "mediaSessionControllerConfig.rtpBufferSize")?;
642        }
643        if let Some(value) = &self.rtp_buffer_initial_count {
644            config.rtp_buffer_initial_count =
645                value.resolve(params, "mediaSessionControllerConfig.rtpBufferInitialCount")?;
646        }
647        if let Some(value) = &self.rtp_buffer_max_count {
648            config.rtp_buffer_max_count =
649                value.resolve(params, "mediaSessionControllerConfig.rtpBufferMaxCount")?;
650        }
651        if let Some(rtp_config) = &self.rtp_session_buffer_config {
652            rtp_config.apply(&mut config.rtp_session_buffer_config, params)?;
653        }
654        if let Some(rtp_config) = &self.rtp_transport_buffer_config {
655            rtp_config.apply(&mut config.rtp_transport_buffer_config, params)?;
656        }
657        Ok(())
658    }
659}
660
661/// Audio frame pool config recipe values.
662#[derive(Debug, Clone, Default, Deserialize)]
663#[serde(rename_all = "camelCase")]
664pub struct RecipeAudioFramePoolConfig {
665    /// Initial pool size.
666    pub initial_size: Option<RecipeUsize>,
667    /// Maximum pool size.
668    pub max_size: Option<RecipeUsize>,
669    /// Sample rate for pooled frames.
670    pub sample_rate: Option<u32>,
671    /// Number of channels for pooled frames.
672    pub channels: Option<u8>,
673    /// Samples per frame.
674    pub samples_per_frame: Option<RecipeUsize>,
675}
676
677impl RecipeAudioFramePoolConfig {
678    fn apply(
679        &self,
680        config: &mut MediaSessionControllerConfig,
681        params: &RecipeParams,
682    ) -> Result<()> {
683        if let Some(value) = &self.initial_size {
684            config.audio_frame_pool.initial_size = value.resolve(
685                params,
686                "mediaSessionControllerConfig.audioFramePool.initialSize",
687            )?;
688        }
689        if let Some(value) = &self.max_size {
690            config.audio_frame_pool.max_size = value.resolve(
691                params,
692                "mediaSessionControllerConfig.audioFramePool.maxSize",
693            )?;
694        }
695        if let Some(value) = self.sample_rate {
696            config.audio_frame_pool.sample_rate = value;
697        }
698        if let Some(value) = self.channels {
699            config.audio_frame_pool.channels = value;
700        }
701        if let Some(value) = &self.samples_per_frame {
702            config.audio_frame_pool.samples_per_frame = value.resolve(
703                params,
704                "mediaSessionControllerConfig.audioFramePool.samplesPerFrame",
705            )?;
706        }
707        Ok(())
708    }
709}