/// 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, 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, default: &'a mut Option, 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 = None; let mut sessions: HashMap = 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 = 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)), ), } } }