1use crate::llm::actions::{
9 protocol_trait::{ActionResult, Protocol, Server},
10 ActionDefinition, Parameter,
11};
12use crate::protocol::log_template::LogTemplate;
13use crate::protocol::EventType;
14use crate::state::app_state::AppState;
15use anyhow::{Context, Result};
16use serde_json::json;
17use std::sync::LazyLock;
18
19pub struct RtpProtocol;
20
21impl RtpProtocol {
22 pub fn new() -> Self {
23 Self
24 }
25}
26
27impl Default for RtpProtocol {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl Protocol for RtpProtocol {
34 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
39 use crate::llm::actions::ParameterDefinition;
40 vec![ParameterDefinition {
41 name: "llm_max_per_minute".to_string(),
42 type_hint: "number".to_string(),
43 description: format!(
44 "Ceiling on model consultations per rolling minute (default {}). A single G.711 \
45 stream is 50 packets per second, so without a ceiling one stream is 50 LLM \
46 calls a second. Datagrams over the ceiling are dropped and nothing is sent, \
47 logged as decision=fail_closed_rate_limited. A script or static event handler \
48 is never charged against this, so 0 - forbidding model consultation entirely - \
49 is the right value for a server driven wholly by handlers.",
50 crate::server::rtp::DEFAULT_LLM_MAX_PER_MINUTE
51 ),
52 required: false,
53 example: json!(30),
54 default: None,
55 }]
56 }
57
58 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
59 vec![send_rtp_audio_action(), send_rtcp_sender_report_action()]
60 }
61 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
62 vec![send_rtp_audio_action(), send_rtcp_sender_report_action()]
63 }
64 fn protocol_name(&self) -> &'static str {
65 "RTP"
66 }
67 fn get_event_types(&self) -> Vec<EventType> {
68 vec![RTP_RECEIVED_EVENT.clone(), RTCP_RECEIVED_EVENT.clone()]
69 }
70 fn stack_name(&self) -> &'static str {
71 "ETH>IP>UDP>RTP"
72 }
73 fn keywords(&self) -> Vec<&'static str> {
74 vec![
75 "rtp",
76 "real-time transport",
77 "rtp media stream",
78 "voip audio",
79 ]
80 }
81 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
82 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
83 ProtocolMetadataV2::builder()
84 .connectionless()
85 .state(DevelopmentState::Experimental)
86 .well_known_udp_port(5004)
87 .deliberately_silent()
90 .implementation(
91 "Manual RFC 3550 packetizer with a G.711 (PCMU/PCMA) synthesis engine; the model \
92 describes content, Rust owns the samples and framing",
93 )
94 .llm_control(
95 "What each stream carries (tone frequency, DTMF digits, silence, or hex-encoded \
96 codec bytes) and the response to inbound RTP/RTCP",
97 )
98 .e2e_testing(
99 "Byte-literal RTP header assertions (mock LLM); interop with ffmpeg/ffprobe via \
100 the rtsp front door",
101 )
102 .notes(
103 "PCMU (PT 0) and PCMA (PT 8) audio genuinely synthesize and decode in ffmpeg. \
104 There is no video codec: video PTs are out of scope. Speech is NOT synthesized — \
105 there is no TTS; ask for a tone/DTMF/silence, or supply raw codec bytes hex-encoded. \
106 RTCP is a minimal SR with no report blocks.",
107 )
108 .build()
109 }
110 fn description(&self) -> &'static str {
111 "RTP media server that synthesizes G.711 audio streams under LLM direction"
112 }
113 fn example_prompt(&self) -> &'static str {
114 "Start an RTP server on port 40000. When a packet arrives, stream a 440 Hz PCMU tone back."
115 }
116 fn group_name(&self) -> &'static str {
117 "Proxy & Network"
118 }
119 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
120 use crate::llm::actions::StartupExamples;
121
122 let script = r#"import json, sys
125data = json.load(sys.stdin)
126event = data["event"]
127if data["event_type_id"] == "rtp_packet_received":
128 actions = [{"type": "send_rtp_audio", "tone_hz": 440, "duration_ms": 200}]
129else:
130 actions = []
131print(json.dumps({"actions": actions}))"#;
132
133 StartupExamples::new(
134 json!({
135 "type": "open_server",
136 "port": 40000,
137 "base_stack": "rtp",
138 "instruction": "RTP media endpoint. When an RTP packet arrives, answer with send_rtp_audio streaming a 440 Hz PCMU tone for 2 seconds, echoing the caller's SSRC."
139 }),
140 json!({
141 "type": "open_server",
142 "port": 40000,
143 "base_stack": "rtp",
144 "event_handlers": [{
145 "event_pattern": "rtp_packet_received",
146 "handler": {"type": "script", "language": "python", "code": script}
147 }]
148 }),
149 json!({
150 "type": "open_server",
151 "port": 40000,
152 "base_stack": "rtp",
153 "event_handlers": [{
154 "event_pattern": "rtp_packet_received",
155 "handler": {
156 "type": "static",
157 "actions": [{
158 "type": "send_rtp_audio",
159 "payload_type": "pcmu",
160 "content": "tone",
161 "tone_hz": 440,
162 "duration_ms": 1000
163 }]
164 }
165 }]
166 }),
167 )
168 }
169}
170
171impl Server for RtpProtocol {
172 fn spawn(
173 &self,
174 ctx: crate::protocol::SpawnContext,
175 ) -> std::pin::Pin<
176 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
177 > {
178 Box::pin(async move {
179 use crate::server::rtp::{RtpConfig, RtpServer};
180 let cfg = RtpConfig::from_params(&ctx.startup_params)?;
181 RtpServer::spawn_with_llm_actions(
182 ctx.legacy_listen_addr(),
183 ctx.llm_client,
184 ctx.state,
185 ctx.status_tx,
186 ctx.server_id,
187 cfg,
188 )
189 .await
190 })
191 }
192
193 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
194 let action_type = action
198 .get("type")
199 .and_then(|v| v.as_str())
200 .context("Missing 'type' field in action")?;
201 match action_type {
202 "send_rtp_audio" => {
203 let codec = action
205 .get("payload_type")
206 .and_then(|v| v.as_str())
207 .map(crate::server::rtp::media::AudioCodec::parse)
208 .unwrap_or(Ok(crate::server::rtp::media::AudioCodec::Pcmu))?;
209 let content = crate::server::rtp::media::parse_audio_content(&action)?;
210 let duration = action
211 .get("duration_ms")
212 .and_then(|v| v.as_u64())
213 .unwrap_or(1000);
214 crate::server::rtp::media::synthesize(codec, &content, duration)?;
215 Ok(ActionResult::NoAction)
216 }
217 "send_rtcp_sender_report" => Ok(ActionResult::NoAction),
218 _ => Err(anyhow::anyhow!("Unknown RTP action: {}", action_type)),
219 }
220 }
221}
222
223pub static RTP_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
224 EventType::new(
225 "rtp_packet_received",
226 "An RTP packet arrived. Answer with send_rtp_audio to stream media back to the caller \
227 (describe the content — a tone, DTMF, or silence — never raw samples). Echo the caller's \
228 ssrc if you want the reply attributed to the same source, or omit it for a fresh one.",
229 json!({
230 "type": "send_rtp_audio",
231 "payload_type": "pcmu",
232 "content": "tone",
233 "tone_hz": 440,
234 "duration_ms": 1000
235 }),
236 )
237 .with_actions(vec![
238 send_rtp_audio_action(),
239 send_rtcp_sender_report_action(),
240 ])
241 .with_log_template(
242 LogTemplate::new()
243 .with_info("RTP pkt pt={payload_type} seq={sequence}")
244 .with_debug("RTP in pt={payload_type} seq={sequence} ssrc={ssrc} len={payload_len}")
245 .with_trace("RTP: {json_pretty(.)}"),
246 )
247});
248
249pub static RTCP_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
250 EventType::new(
251 "rtcp_packet_received",
252 "An RTCP control packet arrived (sender/receiver report, BYE, …). Optionally answer with \
253 send_rtcp_sender_report to report this endpoint's own sending statistics.",
254 json!({
255 "type": "send_rtcp_sender_report",
256 "packet_count": 50,
257 "octet_count": 8000
258 }),
259 )
260 .with_actions(vec![send_rtcp_sender_report_action()])
261 .with_log_template(
262 LogTemplate::new()
263 .with_info("RTCP pkt type={packet_type}")
264 .with_debug("RTCP in type={packet_type} len={length}")
265 .with_trace("RTCP: {json_pretty(.)}"),
266 )
267});
268
269pub fn send_rtp_audio_action() -> ActionDefinition {
270 ActionDefinition {
271 name: "send_rtp_audio".to_string(),
272 description: "Stream synthesized G.711 audio to the caller as RTP packets. Describe the \
273 content structurally; the server owns the samples and the packet framing."
274 .to_string(),
275 parameters: vec![
276 Parameter {
277 name: "payload_type".to_string(),
278 type_hint: "string".to_string(),
279 description:
280 "Codec: \"pcmu\" (PT 0, µ-law) or \"pcma\" (PT 8, A-law). Default pcmu."
281 .to_string(),
282 required: false,
283 },
284 Parameter {
285 name: "content".to_string(),
286 type_hint: "string".to_string(),
287 description: "\"tone\", \"dtmf\", \"silence\", or \"raw\". Default tone."
288 .to_string(),
289 required: false,
290 },
291 Parameter {
292 name: "tone_hz".to_string(),
293 type_hint: "number".to_string(),
294 description: "Tone frequency in Hz (20-3800) when content=tone. Default 440."
295 .to_string(),
296 required: false,
297 },
298 Parameter {
299 name: "digits".to_string(),
300 type_hint: "string".to_string(),
301 description: "DTMF digits (0-9,*,#,A-D) when content=dtmf.".to_string(),
302 required: false,
303 },
304 Parameter {
305 name: "duration_ms".to_string(),
306 type_hint: "number".to_string(),
307 description: "Stream length in milliseconds (1-30000). Default 1000. Ignored \
308 for content=dtmf, whose length is 200 ms per digit, and for \
309 content=raw, whose length is the decoded payload; both are held \
310 to the same 30 s ceiling."
311 .to_string(),
312 required: false,
313 },
314 Parameter {
315 name: "encoding".to_string(),
316 type_hint: "string".to_string(),
317 description: "For content=raw only: must be \"hex\". The server hex-decodes \
318 `samples` into codec bytes for real (never base64, never sniffed)."
319 .to_string(),
320 required: false,
321 },
322 Parameter {
323 name: "samples".to_string(),
324 type_hint: "string".to_string(),
325 description:
326 "For content=raw only: hex-encoded, already codec-encoded payload bytes."
327 .to_string(),
328 required: false,
329 },
330 Parameter {
331 name: "ssrc".to_string(),
332 type_hint: "number".to_string(),
333 description: "Synchronization source id (32-bit). Omit for a random one."
334 .to_string(),
335 required: false,
336 },
337 Parameter {
341 name: "start_sequence".to_string(),
342 type_hint: "number".to_string(),
343 description: "RTP sequence number for the first packet (16-bit). Omit to continue from a random start; set it to resume an existing stream without a gap."
344 .to_string(),
345 required: false,
346 },
347 Parameter {
348 name: "start_timestamp".to_string(),
349 type_hint: "number".to_string(),
350 description: "RTP timestamp for the first packet (32-bit, 8kHz for G.711). Omit for a random start; set it to keep a resumed stream's clock continuous."
351 .to_string(),
352 required: false,
353 },
354 ],
355 example: json!({
356 "type": "send_rtp_audio",
357 "payload_type": "pcmu",
358 "content": "tone",
359 "tone_hz": 440,
360 "duration_ms": 2000,
361 "ssrc": 305419896
362 }),
363 log_template: Some(
364 LogTemplate::new()
365 .with_info("→ RTP {content} {payload_type} {duration_ms}ms")
366 .with_debug("RTP send_rtp_audio content={content} pt={payload_type}"),
367 ),
368 }
369}
370
371pub fn send_rtcp_sender_report_action() -> ActionDefinition {
372 ActionDefinition {
373 name: "send_rtcp_sender_report".to_string(),
374 description: "Send a minimal RTCP Sender Report (RFC 3550 §6.4.1) to the caller."
375 .to_string(),
376 parameters: vec![
377 Parameter {
378 name: "ssrc".to_string(),
379 type_hint: "number".to_string(),
380 description: "Sender SSRC. Omit for a random one.".to_string(),
381 required: false,
382 },
383 Parameter {
384 name: "rtp_timestamp".to_string(),
385 type_hint: "number".to_string(),
386 description: "RTP timestamp reported in the SR. Default 0.".to_string(),
387 required: false,
388 },
389 Parameter {
390 name: "packet_count".to_string(),
391 type_hint: "number".to_string(),
392 description: "Sender's cumulative packet count. Default 0.".to_string(),
393 required: false,
394 },
395 Parameter {
396 name: "octet_count".to_string(),
397 type_hint: "number".to_string(),
398 description: "Sender's cumulative payload octet count. Default 0.".to_string(),
399 required: false,
400 },
401 ],
402 example: json!({
403 "type": "send_rtcp_sender_report",
404 "packet_count": 50,
405 "octet_count": 8000
406 }),
407 log_template: Some(
408 LogTemplate::new()
409 .with_info("→ RTCP SR")
410 .with_debug("RTP send_rtcp_sender_report packets={packet_count}"),
411 ),
412 }
413}