Skip to content

Commit c56d4db

Browse files
committed
feat: implement call record formatter integration and enhance media key formatting[latest]
1 parent 0a265d9 commit c56d4db

11 files changed

Lines changed: 317 additions & 47 deletions

File tree

src/app.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,11 @@ impl AppStateBuilder {
228228
self
229229
}
230230

231+
pub fn with_callrecord_formatter(mut self, formatter: Arc<dyn CallRecordFormatter>) -> Self {
232+
self.callrecord_formatter = Some(formatter);
233+
self
234+
}
235+
231236
pub fn with_proxy_builder(mut self, builder: SipServerBuilder) -> Self {
232237
self.proxy_builder = Some(builder);
233238
self
@@ -349,6 +354,7 @@ impl AppStateBuilder {
349354
backend: backend.clone(),
350355
upload_config: upload_cfg.clone(),
351356
db: Some(db_conn.clone()),
357+
formatter: callrecord_formatter.clone(),
352358
}));
353359
}
354360

@@ -381,7 +387,7 @@ impl AppStateBuilder {
381387
let console_state = match config.console.clone() {
382388
Some(console_config) => Some(
383389
crate::console::ConsoleState::initialize(
384-
callrecord_formatter,
390+
callrecord_formatter.clone(),
385391
db_conn.clone(),
386392
console_config,
387393
)
@@ -442,6 +448,7 @@ impl AppStateBuilder {
442448
.with_storage(core.storage.clone())
443449
.with_sipflow_config(config.sipflow.clone())
444450
.with_sipflow_backend(sipflow_backend_arc.clone())
451+
.with_callrecord_formatter(Some(callrecord_formatter.clone()))
445452
.with_no_bind(self.skip_sip_bind)
446453
.with_skip_migrate(self.skip_migrate)
447454
.with_addon_registry(Some(addon_registry.clone()))

src/callrecord/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,30 @@ pub trait CallRecordFormatter: Send + Sync {
374374
fn format_file_name(&self, record: &CallRecord) -> String;
375375
fn format_transcript_path(&self, record: &CallRecord) -> String;
376376
fn format_media_path(&self, record: &CallRecord, media: &CallRecordMedia) -> String;
377+
378+
fn format_sipflow_media_key(&self, record: &CallRecord) -> String {
379+
format!(
380+
"{}/{}.wav",
381+
record.start_time.format("%Y%m%d"),
382+
record.call_id
383+
)
384+
}
385+
386+
fn format_sipflow_signaling_key(&self, record: &CallRecord) -> String {
387+
format!(
388+
"{}/{}.jsonl",
389+
record.start_time.format("%Y%m%d"),
390+
record.call_id
391+
)
392+
}
393+
394+
fn format_sipflow_media_file_name(&self, record: &CallRecord) -> String {
395+
format!("{}.wav", record.call_id)
396+
}
397+
398+
fn format_sipflow_signaling_file_name(&self, record: &CallRecord) -> String {
399+
format!("{}.jsonl", record.call_id)
400+
}
377401
}
378402

379403
pub struct DefaultCallRecordFormatter {

src/callrecord/sipflow_upload.rs

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use sea_orm::DatabaseConnection;
88
use tracing::{info, warn};
99

1010
use crate::{
11-
callrecord::{CallRecord, CallRecordHook},
11+
callrecord::{CallRecord, CallRecordFormatter, CallRecordHook},
1212
config::SipFlowUploadConfig,
1313
sipflow::SipFlowBackend,
1414
};
@@ -17,6 +17,7 @@ pub struct SipFlowUploadHook {
1717
pub backend: Arc<dyn SipFlowBackend>,
1818
pub upload_config: SipFlowUploadConfig,
1919
pub db: Option<DatabaseConnection>,
20+
pub formatter: Arc<dyn CallRecordFormatter>,
2021
}
2122

2223
#[async_trait]
@@ -29,22 +30,31 @@ impl CallRecordHook for SipFlowUploadHook {
2930
let backend = self.backend.clone();
3031
let upload_config = self.upload_config.clone();
3132
let db = self.db.clone();
33+
let formatter = self.formatter.clone();
3234
let call_id = record.call_id.clone();
3335
let start = Local.from_utc_datetime(&record.start_time.naive_utc());
3436
let end = Local.from_utc_datetime(&record.end_time.naive_utc());
3537
let duration_secs = (record.end_time - record.start_time).num_seconds() as i32;
36-
let date_prefix = record.start_time.format("%Y%m%d").to_string();
38+
39+
let media_key = formatter.format_sipflow_media_key(record);
40+
let signaling_key = formatter.format_sipflow_signaling_key(record);
41+
let media_file_name = formatter.format_sipflow_media_file_name(record);
42+
let signaling_file_name = formatter.format_sipflow_signaling_file_name(record);
3743

3844
crate::utils::spawn(async move {
3945
crate::callrecord::sipflow_upload::do_upload(
4046
backend,
4147
upload_config,
48+
formatter,
4249
db,
4350
&call_id,
4451
start,
4552
end,
4653
duration_secs,
47-
&date_prefix,
54+
&media_key,
55+
&signaling_key,
56+
&media_file_name,
57+
&signaling_file_name,
4858
)
4959
.await;
5060
});
@@ -53,15 +63,20 @@ impl CallRecordHook for SipFlowUploadHook {
5363
}
5464
}
5565

66+
#[allow(clippy::too_many_arguments)]
5667
async fn do_upload(
5768
backend: Arc<dyn SipFlowBackend>,
5869
upload_config: SipFlowUploadConfig,
70+
_formatter: Arc<dyn CallRecordFormatter>,
5971
db: Option<DatabaseConnection>,
6072
call_id: &str,
6173
start: DateTime<Local>,
6274
end: DateTime<Local>,
6375
duration_secs: i32,
64-
date_prefix: &str,
76+
media_key: &str,
77+
signaling_key: &str,
78+
media_file_name: &str,
79+
signaling_file_name: &str,
6580
) {
6681
if let Err(e) = backend.flush().await {
6782
warn!(call_id, "SipFlowUploadHook: flush failed: {e}");
@@ -81,7 +96,8 @@ async fn do_upload(
8196
call_id,
8297
start,
8398
end,
84-
date_prefix,
99+
media_key,
100+
media_file_name,
85101
db.as_ref(),
86102
duration_secs,
87103
)
@@ -94,18 +110,28 @@ async fn do_upload(
94110
};
95111

96112
if signaling {
97-
upload_signaling_flow(&upload_config, backend.as_ref(), call_id, start, end, date_prefix)
98-
.await;
113+
upload_signaling_flow(
114+
&upload_config,
115+
backend.as_ref(),
116+
call_id,
117+
start,
118+
end,
119+
signaling_key,
120+
signaling_file_name,
121+
)
122+
.await;
99123
}
100124
}
101125

126+
#[allow(clippy::too_many_arguments)]
102127
async fn upload_media(
103128
backend: &dyn SipFlowBackend,
104129
upload_config: &SipFlowUploadConfig,
105130
call_id: &str,
106131
start: DateTime<Local>,
107132
end: DateTime<Local>,
108-
date_prefix: &str,
133+
media_key: &str,
134+
media_file_name: &str,
109135
db: Option<&DatabaseConnection>,
110136
duration_secs: i32,
111137
) {
@@ -121,8 +147,6 @@ async fn upload_media(
121147
return;
122148
}
123149

124-
let key = format!("{}/{}.wav", date_prefix, call_id);
125-
126150
let wav_len = wav_bytes.len();
127151
let url_result = match upload_config {
128152
SipFlowUploadConfig::S3 {
@@ -136,9 +160,9 @@ async fn upload_media(
136160
..
137161
} => {
138162
let full_key = if root.is_empty() {
139-
key.clone()
163+
media_key.to_string()
140164
} else {
141-
format!("{}/{}", root.trim_end_matches('/'), key)
165+
format!("{}/{}", root.trim_end_matches('/'), media_key)
142166
};
143167
upload_s3(
144168
vendor, bucket, region, access_key, secret_key, endpoint, &full_key, wav_bytes,
@@ -154,7 +178,7 @@ async fn upload_media(
154178
})
155179
}
156180
SipFlowUploadConfig::Http { url, headers, .. } => {
157-
upload_http(url, headers.as_ref(), call_id, wav_bytes).await
181+
upload_http(url, headers.as_ref(), media_file_name, wav_bytes).await
158182
}
159183
};
160184

@@ -188,13 +212,15 @@ async fn upload_media(
188212
}
189213
}
190214

215+
#[allow(clippy::too_many_arguments)]
191216
async fn upload_signaling_flow(
192217
upload_config: &SipFlowUploadConfig,
193218
backend: &dyn SipFlowBackend,
194219
call_id: &str,
195220
start: DateTime<Local>,
196221
end: DateTime<Local>,
197-
date_prefix: &str,
222+
signaling_key: &str,
223+
signaling_file_name: &str,
198224
) {
199225
let flow_items = match backend.query_flow(call_id, start, end).await {
200226
Ok(items) => items,
@@ -211,8 +237,6 @@ async fn upload_signaling_flow(
211237
let jsonl = crate::sipflow::SipFlowQuery::export_jsonl(&flow_items);
212238
let data = jsonl.into_bytes();
213239

214-
let key = format!("{}/{}.jsonl", date_prefix, call_id);
215-
216240
let result = match upload_config {
217241
SipFlowUploadConfig::S3 {
218242
vendor,
@@ -225,17 +249,17 @@ async fn upload_signaling_flow(
225249
..
226250
} => {
227251
let full_key = if root.is_empty() {
228-
key.clone()
252+
signaling_key.to_string()
229253
} else {
230-
format!("{}/{}", root.trim_end_matches('/'), key)
254+
format!("{}/{}", root.trim_end_matches('/'), signaling_key)
231255
};
232256
upload_s3(
233257
vendor, bucket, region, access_key, secret_key, endpoint, &full_key, data,
234258
)
235259
.await
236260
}
237261
SipFlowUploadConfig::Http { url, headers, .. } => {
238-
upload_http_jsonl(url, headers.as_ref(), call_id, data).await
262+
upload_http_jsonl(url, headers.as_ref(), signaling_file_name, data).await
239263
}
240264
};
241265

@@ -279,13 +303,12 @@ async fn upload_s3(
279303
async fn upload_http(
280304
url: &str,
281305
headers: Option<&std::collections::HashMap<String, String>>,
282-
call_id: &str,
306+
file_name: &str,
283307
data: Vec<u8>,
284308
) -> Result<String> {
285309
let client = reqwest::Client::new();
286-
let file_name = format!("{}.wav", call_id);
287310
let part = reqwest::multipart::Part::bytes(data)
288-
.file_name(file_name)
311+
.file_name(file_name.to_string())
289312
.mime_str("audio/wav")?;
290313
let form = reqwest::multipart::Form::new().part("recording", part);
291314

@@ -298,7 +321,6 @@ async fn upload_http(
298321
let response = req.send().await?;
299322
if response.status().is_success() {
300323
let body = response.text().await.unwrap_or_default();
301-
// Return a URL: use the posted URL or parse from response if it looks like one.
302324
let recording_url = if body.starts_with("http") {
303325
body.trim().to_string()
304326
} else {
@@ -317,13 +339,12 @@ async fn upload_http(
317339
async fn upload_http_jsonl(
318340
url: &str,
319341
headers: Option<&std::collections::HashMap<String, String>>,
320-
call_id: &str,
342+
file_name: &str,
321343
data: Vec<u8>,
322344
) -> Result<()> {
323345
let client = reqwest::Client::new();
324-
let file_name = format!("{}.jsonl", call_id);
325346
let part = reqwest::multipart::Part::bytes(data)
326-
.file_name(file_name)
347+
.file_name(file_name.to_string())
327348
.mime_str("application/jsonl")?;
328349
let form = reqwest::multipart::Form::new().part("signaling", part);
329350

@@ -350,13 +371,12 @@ async fn upload_http_jsonl(
350371
#[cfg(test)]
351372
mod tests {
352373
use super::*;
374+
use crate::callrecord::DefaultCallRecordFormatter;
353375
use crate::sipflow::{SipFlowBackend, SipFlowItem, SipFlowMediaStats};
354376
use chrono::{DateTime, Local};
355377

356378
struct MockBackend {
357-
/// WAV bytes returned by query_media
358379
media: Vec<u8>,
359-
/// Track flush call count
360380
flush_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
361381
}
362382

@@ -429,17 +449,14 @@ mod tests {
429449
media: None,
430450
},
431451
db: None,
452+
formatter: Arc::new(DefaultCallRecordFormatter::default()),
432453
};
433454
let mut record = make_record();
434-
// Hook returns immediately without blocking
435455
let start = std::time::Instant::now();
436456
hook.on_record_completed(&mut record).await.unwrap();
437457
let elapsed = start.elapsed();
438-
// Should return in well under 100ms (no actual work done synchronously)
439458
assert!(elapsed.as_millis() < 100, "hook blocked for {elapsed:?}");
440-
// URL should NOT be set (the background task would set it later)
441459
assert!(record.details.recording_url.is_none());
442-
// Give background task time to run, then check flush was called
443460
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
444461
assert_eq!(flush_count.load(std::sync::atomic::Ordering::Relaxed), 1);
445462
}

src/media/bridge.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,12 @@ impl BridgePeer {
473473
/// endpoint that may not be ready to receive it yet.
474474
pub fn open_caller_gate(&self) {
475475
self.caller_gate.store(true, Ordering::Release);
476+
debug!(bridge_id = %self.id, "Caller gate opened — WebRTC→RTP forwarding enabled");
477+
}
478+
479+
/// Returns whether the caller gate has been opened.
480+
pub fn is_caller_gate_open(&self) -> bool {
481+
self.caller_gate.load(Ordering::Acquire)
476482
}
477483

478484
/// Setup the bridge by adding sample tracks to both sides for forwarding

src/proxy/proxy_call/reporter.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,15 @@ impl CallReporter {
213213
}) = self.server.sipflow_config.as_ref()
214214
&& media.unwrap_or(true)
215215
{
216-
let date_prefix = start_time.format("%Y%m%d").to_string();
217-
let key = format!("{}/{}.wav", date_prefix, self.context.session_id);
216+
let key = if let Some(ref formatter) = self.server.callrecord_formatter {
217+
let mut tmp = CallRecord::default();
218+
tmp.call_id = self.context.session_id.clone();
219+
tmp.start_time = start_time;
220+
formatter.format_sipflow_media_key(&tmp)
221+
} else {
222+
let date_prefix = start_time.format("%Y%m%d").to_string();
223+
format!("{}/{}.wav", date_prefix, self.context.session_id)
224+
};
218225
let full_key = if root.is_empty() {
219226
key
220227
} else {

0 commit comments

Comments
 (0)