287 lines
10 KiB
Rust
287 lines
10 KiB
Rust
/// Audio transcoding bridge for smartrust.
|
|
///
|
|
/// Thin CLI wrapper around `codec-lib`. Handles Opus ↔ G.722 ↔ PCMU transcoding.
|
|
///
|
|
/// Protocol:
|
|
/// -> {"id":"1","method":"init","params":{}}
|
|
/// <- {"id":"1","success":true,"result":{}}
|
|
/// -> {"id":"2","method":"create_session","params":{"session_id":"call-abc"}}
|
|
/// <- {"id":"2","success":true,"result":{}}
|
|
/// -> {"id":"3","method":"transcode","params":{"session_id":"call-abc","data_b64":"...","from_pt":111,"to_pt":9}}
|
|
/// <- {"id":"3","success":true,"result":{"data_b64":"..."}}
|
|
/// -> {"id":"4","method":"destroy_session","params":{"session_id":"call-abc"}}
|
|
/// <- {"id":"4","success":true,"result":{}}
|
|
|
|
use base64::engine::general_purpose::STANDARD as B64;
|
|
use base64::Engine as _;
|
|
use codec_lib::{codec_sample_rate, TranscodeState};
|
|
use serde::Deserialize;
|
|
use std::collections::HashMap;
|
|
use std::io::{self, BufRead, Write};
|
|
|
|
#[derive(Deserialize)]
|
|
struct Request {
|
|
id: String,
|
|
method: String,
|
|
#[serde(default)]
|
|
params: serde_json::Value,
|
|
}
|
|
|
|
fn respond(
|
|
out: &mut impl Write,
|
|
id: &str,
|
|
success: bool,
|
|
result: Option<serde_json::Value>,
|
|
error: Option<&str>,
|
|
) {
|
|
let mut resp = serde_json::json!({ "id": id, "success": success });
|
|
if let Some(r) = result {
|
|
resp["result"] = r;
|
|
}
|
|
if let Some(e) = error {
|
|
resp["error"] = serde_json::Value::String(e.to_string());
|
|
}
|
|
let _ = writeln!(out, "{}", resp);
|
|
let _ = out.flush();
|
|
}
|
|
|
|
/// Resolve a session: if session_id is provided, look it up in the sessions map;
|
|
/// otherwise fall back to the default state (backward compat with `init`).
|
|
fn get_session<'a>(
|
|
sessions: &'a mut HashMap<String, TranscodeState>,
|
|
default: &'a mut Option<TranscodeState>,
|
|
params: &serde_json::Value,
|
|
) -> Option<&'a mut TranscodeState> {
|
|
if let Some(sid) = params.get("session_id").and_then(|v| v.as_str()) {
|
|
sessions.get_mut(sid)
|
|
} else {
|
|
default.as_mut()
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let stdin = io::stdin();
|
|
let stdout = io::stdout();
|
|
let mut out = io::BufWriter::new(stdout.lock());
|
|
|
|
let _ = writeln!(out, r#"{{"event":"ready","data":{{}}}}"#);
|
|
let _ = out.flush();
|
|
|
|
let mut default_state: Option<TranscodeState> = None;
|
|
let mut sessions: HashMap<String, TranscodeState> = HashMap::new();
|
|
|
|
for line in stdin.lock().lines() {
|
|
let line = match line {
|
|
Ok(l) if !l.trim().is_empty() => l,
|
|
Ok(_) => continue,
|
|
Err(_) => break,
|
|
};
|
|
|
|
let req: Request = match serde_json::from_str(&line) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
respond(&mut out, "", false, None, Some(&format!("parse: {e}")));
|
|
continue;
|
|
}
|
|
};
|
|
|
|
match req.method.as_str() {
|
|
"init" => match TranscodeState::new() {
|
|
Ok(s) => {
|
|
default_state = Some(s);
|
|
respond(&mut out, &req.id, true, Some(serde_json::json!({})), None);
|
|
}
|
|
Err(e) => respond(&mut out, &req.id, false, None, Some(&e)),
|
|
},
|
|
|
|
"create_session" => {
|
|
let session_id = match req.params.get("session_id").and_then(|v| v.as_str()) {
|
|
Some(s) => s.to_string(),
|
|
None => {
|
|
respond(&mut out, &req.id, false, None, Some("missing session_id"));
|
|
continue;
|
|
}
|
|
};
|
|
if sessions.contains_key(&session_id) {
|
|
respond(&mut out, &req.id, true, Some(serde_json::json!({})), None);
|
|
continue;
|
|
}
|
|
match TranscodeState::new() {
|
|
Ok(s) => {
|
|
sessions.insert(session_id, s);
|
|
respond(&mut out, &req.id, true, Some(serde_json::json!({})), None);
|
|
}
|
|
Err(e) => respond(&mut out, &req.id, false, None, Some(&e)),
|
|
}
|
|
}
|
|
|
|
"destroy_session" => {
|
|
let session_id = match req.params.get("session_id").and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => {
|
|
respond(&mut out, &req.id, false, None, Some("missing session_id"));
|
|
continue;
|
|
}
|
|
};
|
|
sessions.remove(session_id);
|
|
respond(&mut out, &req.id, true, Some(serde_json::json!({})), None);
|
|
}
|
|
|
|
"transcode" => {
|
|
let st = match get_session(&mut sessions, &mut default_state, &req.params) {
|
|
Some(s) => s,
|
|
None => {
|
|
respond(
|
|
&mut out,
|
|
&req.id,
|
|
false,
|
|
None,
|
|
Some("not initialized (no session or default state)"),
|
|
);
|
|
continue;
|
|
}
|
|
};
|
|
let data_b64 = match req.params.get("data_b64").and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => {
|
|
respond(&mut out, &req.id, false, None, Some("missing data_b64"));
|
|
continue;
|
|
}
|
|
};
|
|
let from_pt =
|
|
req.params.get("from_pt").and_then(|v| v.as_u64()).unwrap_or(0) as u8;
|
|
let to_pt = req.params.get("to_pt").and_then(|v| v.as_u64()).unwrap_or(0) as u8;
|
|
let direction = req.params.get("direction").and_then(|v| v.as_str());
|
|
|
|
let data = match B64.decode(data_b64) {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
respond(
|
|
&mut out,
|
|
&req.id,
|
|
false,
|
|
None,
|
|
Some(&format!("b64: {e}")),
|
|
);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
match st.transcode(&data, from_pt, to_pt, direction) {
|
|
Ok(result) => {
|
|
respond(
|
|
&mut out,
|
|
&req.id,
|
|
true,
|
|
Some(serde_json::json!({ "data_b64": B64.encode(&result) })),
|
|
None,
|
|
);
|
|
}
|
|
Err(e) => respond(&mut out, &req.id, false, None, Some(&e)),
|
|
}
|
|
}
|
|
|
|
"encode_pcm" => {
|
|
let st = match get_session(&mut sessions, &mut default_state, &req.params) {
|
|
Some(s) => s,
|
|
None => {
|
|
respond(
|
|
&mut out,
|
|
&req.id,
|
|
false,
|
|
None,
|
|
Some("not initialized (no session or default state)"),
|
|
);
|
|
continue;
|
|
}
|
|
};
|
|
let data_b64 = match req.params.get("data_b64").and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => {
|
|
respond(&mut out, &req.id, false, None, Some("missing data_b64"));
|
|
continue;
|
|
}
|
|
};
|
|
let sample_rate = req
|
|
.params
|
|
.get("sample_rate")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(22050) as u32;
|
|
let to_pt = req.params.get("to_pt").and_then(|v| v.as_u64()).unwrap_or(9) as u8;
|
|
|
|
let data = match B64.decode(data_b64) {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
respond(
|
|
&mut out,
|
|
&req.id,
|
|
false,
|
|
None,
|
|
Some(&format!("b64: {e}")),
|
|
);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
if data.len() % 2 != 0 {
|
|
respond(
|
|
&mut out,
|
|
&req.id,
|
|
false,
|
|
None,
|
|
Some("PCM data has odd byte count (expected 16-bit LE samples)"),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
let pcm: Vec<i16> = data
|
|
.chunks_exact(2)
|
|
.map(|c| i16::from_le_bytes([c[0], c[1]]))
|
|
.collect();
|
|
|
|
let target_rate = codec_sample_rate(to_pt);
|
|
let resampled = match st.resample(&pcm, sample_rate, target_rate) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
respond(&mut out, &req.id, false, None, Some(&e));
|
|
continue;
|
|
}
|
|
};
|
|
|
|
match st.encode_from_pcm(&resampled, to_pt) {
|
|
Ok(encoded) => {
|
|
respond(
|
|
&mut out,
|
|
&req.id,
|
|
true,
|
|
Some(serde_json::json!({ "data_b64": B64.encode(&encoded) })),
|
|
None,
|
|
);
|
|
}
|
|
Err(e) => {
|
|
respond(&mut out, &req.id, false, None, Some(&e));
|
|
}
|
|
}
|
|
}
|
|
|
|
"encode" | "decode" => {
|
|
respond(
|
|
&mut out,
|
|
&req.id,
|
|
false,
|
|
None,
|
|
Some("use 'transcode' command instead"),
|
|
);
|
|
}
|
|
|
|
_ => respond(
|
|
&mut out,
|
|
&req.id,
|
|
false,
|
|
None,
|
|
Some(&format!("unknown: {}", req.method)),
|
|
),
|
|
}
|
|
}
|
|
}
|