use anyhow::{bail, Context, Result};
use iris_chat_core::{AppAction, AppReconciler, AppUpdate, CallAudioCodec, FfiApp};
use serde_json::json;
use std::io::{self, BufRead, Write};
use std::time::{Duration, Instant};
mod iris_call_fixture_account;
mod iris_call_fixture_mode;
struct Updates(flume::Sender<AppUpdate>);
impl AppReconciler for Updates {
fn reconcile(&self, update: AppUpdate) {
let _ = self.0.try_send(update);
}
}
fn emit(value: serde_json::Value) -> Result<()> {
println!("{value}");
io::stdout().flush().context("flush fixture output")
}
fn main() -> Result<()> {
let mut args = std::env::args().skip(1);
let dir = args
.next()
.context("usage: iris-call-fixture <fresh-data-dir>")?;
let isolated_control = std::env::var("IRIS_CALL_ISOLATED_CONTROL").as_deref() == Ok("1");
let mode = iris_call_fixture_mode::Mode::parse(&args.collect::<Vec<_>>(), isolated_control)?;
if mode.resume_device.is_none()
&& std::path::Path::new(&dir)
.read_dir()
.is_ok_and(|mut entries| entries.next().is_some())
{
bail!("Call fixture requires a fresh data directory");
}
if mode.normal_persistent {
iris_call_fixture_mode::private_directory(std::path::Path::new(&dir))?;
}
let receipt = std::path::Path::new(&dir).join("fixture-account-bundle.json");
let restore = mode
.resume_device
.as_deref()
.map(|peer| match mode.resume_owner.as_deref() {
Some(owner) => iris_call_fixture_account::restore_exact(&receipt, owner, peer),
None => iris_call_fixture_account::restore(&receipt, peer),
})
.transpose()?;
let app = FfiApp::new(dir, String::new(), String::new());
let (tx, updates) = flume::bounded(96);
app.listen_for_updates(Box::new(Updates(tx)));
if isolated_control {
app.dispatch(AppAction::SetNearbyEnabled { enabled: false });
app.dispatch(AppAction::SetNearbyLanEnabled { enabled: false });
app.dispatch(AppAction::SetNearbyBluetoothEnabled { enabled: false });
}
app.dispatch(restore.unwrap_or_else(|| {
AppAction::CreateAccount {
name: if mode.normal_persistent {
"Saved receiver test"
} else {
"Call test"
}
.into(),
}
}));
if isolated_control {
app.dispatch(AppAction::SetNostrRelays {
relay_urls: vec![std::env::var("IRIS_DEMO_RELAYS")?],
});
}
if !isolated_control && !mode.normal_persistent {
app.dispatch(AppAction::SetNearbyLanEnabled { enabled: true });
}
if let Ok(url) = std::env::var("IRIS_CALL_PUSH_SERVER_URL") {
app.dispatch(AppAction::SetMobilePushServerUrl { url });
}
app.dispatch(AppAction::CreatePublicInvite);
let deadline = Instant::now() + Duration::from_secs(30);
let mut identity_saved = !mode.persist_identity || mode.resume_device.is_some();
loop {
if !identity_saved {
for update in updates.try_iter() {
if let AppUpdate::PersistAccountBundle {
owner_nsec,
owner_pubkey_hex,
device_nsec,
..
} = update
{
iris_call_fixture_account::save(
&receipt,
owner_nsec.as_deref(),
&owner_pubkey_hex,
&device_nsec,
)?;
identity_saved = true;
break;
}
}
}
let state = app.state();
if let (Some(account), Some(invite), true) =
(state.account, state.public_invite, identity_saved)
{
if mode.normal_persistent {
anyhow::ensure!(
state.preferences.nostr_relay_urls == vec![std::env::var("IRIS_DEMO_RELAYS")?],
"Saved fixture message server differs from the preserved setting"
);
}
emit(
json!({"event":"ready","owner":account.public_key_hex,"device":account.device_public_key_hex,"device_npub":account.device_npub,"invite":invite.url}),
)?;
break;
}
if Instant::now() > deadline {
bail!("Account and invite did not become ready: {:?}", state.toast);
}
std::thread::sleep(Duration::from_millis(20));
}
let (commands_tx, commands) = flume::bounded(16);
std::thread::spawn(move || {
for line in io::stdin().lock().lines() {
let Ok(line) = line else {
break;
};
if commands_tx.send(line).is_err() {
break;
}
}
});
let mut auto_answer = true;
let mut answer_voice = false;
let mut last_call = String::new();
let mut last_key_request = (String::new(), 0u32);
let mut audio = 0u64;
let mut video = 0u64;
let mut lan_owners = Vec::<String>::new();
let mut audio_nonzero = 0u64;
let audio_codec = CallAudioCodec::new()?;
let mut audio_playout_at = Instant::now();
#[cfg(feature = "desktop-media")]
let desktop_video = if std::env::var_os("IRIS_CALL_DESKTOP_CODEC").is_some() {
let (send, recv) = flume::bounded::<(String, u32, bool, u64, Vec<u8>, u32, u32)>(8);
let (out_send, out_recv) = flume::bounded(16);
let mut codec =
iris_chat_core::DesktopVideoTranscoder::new().map_err(anyhow::Error::msg)?;
std::thread::spawn(move || {
let mut active = String::new();
while let Ok((id, seq, key, timestamp, data, target, generation)) = recv.recv() {
if active != id {
let Ok(fresh) = iris_chat_core::DesktopVideoTranscoder::new() else {
break;
};
codec = fresh;
active = id.clone();
}
for event in codec.receive(seq, key, data, target, generation) {
if out_send.send((id.clone(), timestamp, event)).is_err() {
return;
}
}
}
});
Some((send, out_recv))
} else {
None
};
#[cfg(not(feature = "desktop-media"))]
if std::env::var_os("IRIS_CALL_DESKTOP_CODEC").is_some() {
bail!("Build the call fixture with desktop-media for desktop codec interoperability");
}
loop {
match commands.recv_timeout(Duration::from_millis(10)) {
Ok(line) => {
let parts = line.split_whitespace().collect::<Vec<_>>();
match parts.as_slice() {
["accept", owner] => {
app.dispatch(AppAction::CreateChat {
peer_input: (*owner).into(),
});
app.dispatch(AppAction::SetMessageRequestAccepted {
chat_id: (*owner).into(),
});
emit(json!({"event":"accepted"}))?;
}
["call", owner, kind] => app.dispatch(AppAction::StartCall {
chat_id: (*owner).into(),
video: *kind == "video",
}),
["message", owner, words @ ..] if !words.is_empty() => {
app.dispatch(AppAction::SendMessage {
chat_id: (*owner).into(),
text: words.join(" "),
});
emit(json!({"event":"message-sent"}))?;
}
["answer", kind] => {
auto_answer = true;
answer_voice = *kind == "voice";
}
["manual"] => auto_answer = false,
["peer", owner] => {
let ready = app.peer_profile_debug((*owner).into()).is_some_and(|peer| {
peer.roster_device_count > 0 && peer.active_session_count > 0
});
emit(json!({"event":"peer","ready":ready}))?;
}
["status"] => {
let state = app.state();
let call=state.call.map(|s|json!({"id":s.call_id,"phase":s.phase,"video":s.video_capable,"muted":s.remote_muted}));
emit(
json!({"event":"status","call":call,"audio_frames":audio,"video_frames":video,"nonzero_audio_frames":audio_nonzero,
"lan_owners":lan_owners}),
)?;
}
["health"] if isolated_control => {
let raw: serde_json::Value =
serde_json::from_str(&app.export_support_bundle_json())?;
if raw["ffi_queue"]["core_support_bundle_timed_out"].as_bool()
!= Some(false)
{
bail!("Control support query unavailable");
}
let expected = std::env::var("IRIS_DEMO_RELAYS")?;
let transport = &raw["relay_transport"];
emit(json!({"event":"health",
"expected_relay_matches":raw["relay_urls"] == json!([expected]),
"connected_relay_count":transport["connected_relay_count"].as_u64().context("Missing relay count")?,
"pending_relay_publish_count":transport["pending_relay_publish_count"].as_u64().context("Missing outbox count")?,
"fips_transport":raw.get("fips_transport").context("Missing FIPS diagnostics")?,
}))?;
}
["end"] => {
if let Some(c) = app.state().call {
app.dispatch(AppAction::EndCall { call_id: c.call_id });
}
}
["stop"] => break,
_ => emit(json!({"event":"error","reason":"Unknown fixture command"}))?,
}
}
Err(flume::RecvTimeoutError::Disconnected) => break,
Err(flume::RecvTimeoutError::Timeout) => {}
}
for update in updates.try_iter().take(128) {
if let AppUpdate::NearbyPeersChanged {
snapshot,
lan_peer_ids,
..
} = &update
{
lan_owners = snapshot
.peers
.iter()
.filter(|peer| lan_peer_ids.contains(&peer.id))
.filter_map(|peer| peer.owner_pubkey_hex.clone())
.collect();
}
if let AppUpdate::CallMedia {
call_id,
kind,
sequence,
timestamp_us,
key_frame,
data,
} = update
{
if kind == 1 {
audio += 1;
audio_codec.queue(sequence, data.clone());
} else if kind == 2 {
video += 1;
}
#[cfg(feature = "desktop-media")]
if kind == 2 {
if let (Some((send, _)), Some(call)) = (&desktop_video, app.state().call) {
if send
.try_send((
call_id.clone(),
sequence,
key_frame,
timestamp_us,
data,
call.target_bitrate_bps,
call.key_frame_generation,
))
.is_err()
{
app.dispatch(AppAction::RequestCallKeyFrame { call_id });
}
continue;
}
}
app.dispatch(AppAction::SendCallMedia {
call_id,
kind,
timestamp_us,
key_frame,
data,
});
}
}
#[cfg(feature = "desktop-media")]
if let Some((_, recv)) = &desktop_video {
for (call_id, timestamp_us, event) in recv.try_iter().take(16) {
match event {
iris_chat_core::DesktopCallEvent::Encoded {
kind,
key_frame,
data,
..
} => app.dispatch(AppAction::SendCallMedia {
call_id,
kind,
timestamp_us,
key_frame,
data,
}),
iris_chat_core::DesktopCallEvent::RequestKeyFrame => {
app.dispatch(AppAction::RequestCallKeyFrame { call_id })
}
_ => {}
}
}
}
if audio_playout_at.elapsed() >= Duration::from_millis(20) {
audio_playout_at = Instant::now();
if audio_codec.playout().iter().any(|v| v.unsigned_abs() > 10) {
audio_nonzero += 1;
}
}
if let Some(call) = app.state().call {
if call.key_frame_generation > 0
&& last_key_request != (call.call_id.clone(), call.key_frame_generation)
{
app.dispatch(AppAction::RequestCallKeyFrame {
call_id: call.call_id.clone(),
});
last_key_request = (call.call_id.clone(), call.key_frame_generation);
}
let state = format!("{}:{}", call.call_id, call.phase);
if last_call != state {
emit(
json!({"event":"call","id":call.call_id,"phase":call.phase,"video":call.video_capable}),
)?;
last_call = state;
if call.phase == "connected" {
app.dispatch(AppAction::SetCallMediaConnected {
call_id: call.call_id.clone(),
connected: true,
});
}
if auto_answer && call.phase == "incoming" {
app.dispatch(if answer_voice {
AppAction::AnswerCallWithVoice {
call_id: call.call_id,
}
} else {
AppAction::AnswerCall {
call_id: call.call_id,
}
});
}
}
}
}
app.shutdown();
emit(json!({"event":"stopped"}))
}