use crate::audio;
use crate::proto::{
self, Command, FromWorker, Headers, Query, QueryEnvelope, Ready, Reply, ReplyResult,
RxAudioStarted, ToWorker, WireEvent, WireMediaStats, WireSavedAudio,
};
use anyhow::{Context, Result, bail};
use ringo_core::backend::{Backend, BaresipBackend};
use ringo_core::phone::Phone;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{RecvTimeoutError, SyncSender};
use std::thread::JoinHandle;
use std::time::Duration;
enum Out {
Control(FromWorker),
Audio(Vec<i16>),
}
const HEADER_POLL_INTERVAL: Duration = Duration::from_millis(150);
const WRITER_CHANNEL_BOUND: usize = 1024;
pub fn run() -> Result<()> {
let stdin = std::io::stdin();
let mut stdin = stdin.lock();
let handshake: proto::Handshake = match proto::read_frame(&mut stdin) {
Ok(Some((proto::FRAME_CONTROL, payload))) => {
serde_json::from_slice(&payload).context("parse config handshake")?
}
Ok(Some(_)) => bail!("worker: first frame was not the config handshake"),
Ok(None) => bail!("worker stdin closed before config handshake"),
Err(e) => return Err(e).context("read config handshake"),
};
if handshake.proto_version != proto::PROTO_VERSION {
bail!(
"worker: protocol version mismatch (parent {}, worker {}); rebuild so \
both sides share one binary",
handshake.proto_version,
proto::PROTO_VERSION
);
}
let config = handshake.config;
init_logging(&config.name);
let account = config.account;
let options = config.options;
let username = account.username.clone();
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.context("build worker tokio runtime")?;
let session = BaresipBackend
.spawn_session(rt.handle(), &config.name, &account, &options)
.with_context(|| format!("spawn backend for `{}`", config.name))?;
let events = session.events;
let phone = session.phone;
let header_poll = session.header_poll;
let handle = session.handle;
let (tx, rx) = std::sync::mpsc::sync_channel::<Out>(WRITER_CHANNEL_BOUND);
let writer = std::thread::spawn(move || {
let stdout = std::io::stdout();
while let Ok(msg) = rx.recv() {
let mut out = stdout.lock();
let res = match msg {
Out::Control(c) => match serde_json::to_vec(&c) {
Ok(bytes) => proto::write_frame(&mut out, proto::FRAME_CONTROL, &bytes),
Err(e) => {
ringo_core::rlog!(Warn, "serialize outbound: {e}");
continue;
}
},
Out::Audio(samples) => {
proto::write_frame(&mut out, proto::FRAME_AUDIO, &proto::pcm_to_bytes(&samples))
}
};
if res.is_err() {
break;
}
}
});
let _ = tx.send(Out::Control(FromWorker::Ready(Ready { ready: true })));
let ev_tx = tx.clone();
let event_bridge = std::thread::spawn(move || {
while let Ok(event) = events.recv() {
if ev_tx
.send(Out::Control(FromWorker::Event(WireEvent::from(&event))))
.is_err()
{
break;
}
}
});
let stop = Arc::new(AtomicBool::new(false));
let header_thread = header_poll.map(|poll| {
let hdr_tx = tx.clone();
let stop = Arc::clone(&stop);
std::thread::spawn(move || {
while !stop.load(Ordering::Relaxed) {
std::thread::sleep(HEADER_POLL_INTERVAL);
if let Some(headers) = poll() {
if !headers.is_empty()
&& hdr_tx
.send(Out::Control(FromWorker::Headers(Headers { headers })))
.is_err()
{
break;
}
}
}
})
});
let rx_stop = Arc::new(AtomicBool::new(false));
let mut rx_forwarder: Option<JoinHandle<()>> = None;
loop {
let frame = match proto::read_frame(&mut stdin) {
Ok(Some(f)) => f,
Ok(None) => break, Err(e) => {
ringo_core::rlog!(Warn, "worker stdin read error: {e}");
break;
}
};
match frame {
(proto::FRAME_AUDIO, payload) => {
ringo_core::push_audio(&username, &proto::bytes_to_pcm(&payload));
}
(proto::FRAME_CONTROL, payload) => match serde_json::from_slice::<ToWorker>(&payload) {
Ok(ToWorker::Cmd(Command::Shutdown)) => break,
Ok(ToWorker::Cmd(Command::StartTxAudio { rate })) => {
ringo_core::start_audio_stream(&username, rate);
}
Ok(ToWorker::Cmd(Command::StartRxAudio)) => {
if rx_forwarder.is_none() {
rx_forwarder = Some(spawn_rx_forwarder(
&username,
tx.clone(),
Arc::clone(&rx_stop),
));
}
}
Ok(ToWorker::Cmd(Command::SetRecording { on })) => {
ringo_core::set_recording(&username, on);
}
Ok(ToWorker::Cmd(cmd)) => dispatch(phone.as_ref(), cmd),
Ok(ToWorker::Query(QueryEnvelope { id, query })) => {
let result = answer(phone.as_ref(), &username, query);
if tx
.send(Out::Control(FromWorker::Reply(Reply { reply: id, result })))
.is_err()
{
break;
}
}
Err(e) => ringo_core::rlog!(
Warn,
"ignoring malformed control frame ({} bytes): {e}",
payload.len()
),
},
(_kind, _) => {} }
}
stop.store(true, Ordering::Relaxed);
rx_stop.store(true, Ordering::Relaxed);
if let Some(h) = header_thread {
let _ = h.join();
}
if let Some(h) = rx_forwarder {
let _ = h.join();
}
drop(phone);
drop(handle); ringo_core::shutdown();
let _ = event_bridge.join();
drop(tx);
let _ = writer.join();
Ok(())
}
fn spawn_rx_forwarder(
username: &str,
tx: SyncSender<Out>,
stop: Arc<AtomicBool>,
) -> JoinHandle<()> {
let rx = ringo_core::subscribe_received_audio(username);
std::thread::spawn(move || {
let mut announced_rate: Option<u32> = None;
while !stop.load(Ordering::Relaxed) {
match rx.recv_timeout(Duration::from_millis(200)) {
Ok(frame) => {
if announced_rate != Some(frame.rate) {
announced_rate = Some(frame.rate);
let started = FromWorker::RxAudioStarted(RxAudioStarted {
rx_audio_rate: frame.rate,
});
if tx.send(Out::Control(started)).is_err() {
break;
}
}
if tx.send(Out::Audio(frame.samples)).is_err() {
break;
}
}
Err(RecvTimeoutError::Timeout) => continue, Err(RecvTimeoutError::Disconnected) => break,
}
}
})
}
fn init_logging(name: &str) {
if let Some(t) = std::env::var_os("RINGO_AGENT_LOG").and_then(|v| v.into_string().ok()) {
match t.as_str() {
"" => {}
"-" => ringo_core::log::init_stderr(),
path => ringo_core::log::init_file(per_agent_path(path, name, std::process::id())),
}
}
if let Some(t) = std::env::var_os("RINGO_AGENT_SIPTRACE").and_then(|v| v.into_string().ok()) {
match t.as_str() {
"" => {}
"-" => ringo_core::sip_trace_stderr(),
path => ringo_core::sip_trace_file(per_agent_path(path, name, std::process::id())),
}
}
}
pub fn per_agent_path(base: &str, name: &str, pid: u32) -> std::path::PathBuf {
let safe: String = name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect();
let tag = format!("{safe}.{pid}");
let p = std::path::Path::new(base);
match p.extension().and_then(|e| e.to_str()) {
Some(ext) => p.with_extension(format!("{tag}.{ext}")),
None => p.with_extension(tag),
}
}
fn dispatch(phone: &dyn Phone, cmd: Command) {
match cmd {
Command::Register { aor, regint } => phone.register(&aor, regint),
Command::Dial { number } => phone.dial(&number),
Command::Hangup => phone.hangup(),
Command::HangupAll => phone.hangup_all(),
Command::Accept => phone.accept(),
Command::Hold => phone.hold(),
Command::Resume => phone.resume(),
Command::Mute => phone.mute(),
Command::Dtmf { digit } => phone.send_dtmf(digit),
Command::SwitchLine { line } => phone.switch_line(line),
Command::Transfer { uri } => phone.transfer(&uri),
Command::AttendedTransferStart { uri } => phone.attended_transfer_start(&uri),
Command::AttendedTransferExec => phone.attended_transfer_exec(),
Command::AttendedTransferAbort => phone.attended_transfer_abort(),
Command::AddHeader { key, value } => phone.add_header(&key, &value),
Command::RmHeader { key } => phone.rm_header(&key),
Command::SetAudioSource { spec } => phone.set_audio_source(&spec),
Command::ArmInviteResponse {
scode,
reason,
headers,
} => phone.arm_invite_response(scode, &reason, headers),
Command::DisarmInviteResponse => phone.disarm_invite_response(),
Command::StartTxAudio { .. }
| Command::StartRxAudio
| Command::SetRecording { .. }
| Command::Shutdown => {}
}
}
fn answer(phone: &dyn Phone, username: &str, query: Query) -> ReplyResult {
match query {
Query::MediaStats => ReplyResult::MediaStats(phone.media_stats().map(WireMediaStats::from)),
Query::ReceivedDtmf => ReplyResult::Dtmf(phone.received_dtmf()),
Query::SaveAudio { prefix } => {
let mut written = Vec::new();
for (tag, captured) in [
("sent", ringo_core::sent_audio(username)),
("recv", ringo_core::received_audio(username)),
] {
let Some((samples, srate)) = captured else {
continue;
};
if samples.is_empty() {
continue;
}
let truncated =
samples.len() >= srate as usize * ringo_core::RECORDING_MAX_SECS as usize;
let path = format!("{prefix}-{tag}.wav");
match audio::write_wav(std::path::Path::new(&path), &samples, srate) {
Ok(()) => written.push(WireSavedAudio {
path,
kind: tag.into(),
samples: samples.len(),
srate,
truncated,
}),
Err(e) => ringo_core::rlog!(Warn, "save {path}: {e}"),
}
}
ReplyResult::SavedAudio(written)
}
Query::CallCount => ReplyResult::CallCount(ringo_core::call_count() as u32),
}
}