code wiki / (root) / nx_call.nx

nx_call.nx source

↩ module page · 218 lines · 7496 B

1// nx_call.nx -- 1:1 audio + chat call. 2// 3// Per NISHI_COMMS_ROADMAP.md (the Texas <-> Ukraine son-talking-to- 4// girlfriend use case): a sovereign peer-to-peer voice call with 5// optional chat alongside, no Telegram in the path. 6// 7// COMPOSES (does not subclass): 8// nx_audio_session -- voice over the NishiVoice codec 9// nx_chat_session -- text overlay 10// nx_media_session -- shared lifecycle vtable 11// 12// Per cardinal feedback-bits-up-exceed-never-match: zero new 13// transport logic here. The call just wires two sessions together 14// behind a tiny API surface so the consumer (web UI / CLI / IoT 15// tablet) doesn't deal with two separate session lifecycles. 16// 17// Future composition (queued, not in v1): 18// nx_signaling.nx -- SDP exchange + ICE candidates 19// nx_p2p_mesh.nx -- single-peer p2p path (mesh degenerates to 1:1) 20// nx_turn_client.nx -- sovereign TURN client (after L3-A.5.2 lands) 21 22// nx_safety_envelope: 23// intended_use: 1:1 sovereign audio + chat call consumer 24// sil_target: SIL2 25// evidence: composes nx_audio_session + nx_chat_session, 26// both proven by their own end-to-end smokes 27// verdict: NOT_YET_EVALUATED -- pending nx_call smoke 28 29import "nx_syscalls_x86_64.nx" 30import "nx_media_session.nx" 31import "nx_codec.nx" 32import "nx_audio_capture.nx" 33import "nx_audio_playback.nx" 34import "nx_audio_session.nx" 35import "nx_chat_ring.nx" 36import "nx_chat_session.nx" 37 38// ---- Sealed enum: call state ---- 39 40const NX_CALL_STATE_UNKNOWN: i64 = 0 41const NX_CALL_STATE_IDLE: i64 = 1 42const NX_CALL_STATE_DIALING: i64 = 2 43const NX_CALL_STATE_LIVE: i64 = 3 // both sessions ACTIVE 44const NX_CALL_STATE_ENDED: i64 = 4 45const NX_CALL_STATE_FAILED: i64 = 5 46const NX_CALL_STATE_N: i64 = 6 47 48// ---- The struct ---- 49// 50// Holds pointers to BOTH composed sessions plus per-call identity. 51// The audio and chat sessions are independently constructible; the 52// caller decides which they want. Chat is optional in v1. 53 54struct nx_call { 55 call_id: i64, // monotonic per-process 56 remote_peer: i64, // assigned via signaling; 0 until LIVE 57 started_ms: i64, 58 state: i64, // NX_CALL_STATE_* 59 audio: *nx_audio_session, // required 60 chat: *nx_chat_session, // optional; null if voice-only 61} 62 63const NX_CALL_BYTES: i64 = 48 64 65// ---- Module-level id counter ---- 66 67func _nx_call_next_id() -> i64 { 68 let slot: *i64 = sys_mmap(8) as *i64 69 let v: i64 = slot[0] + 1 70 slot[0] = v 71 return v 72} 73 74// ---- Construction ---- 75 76func nx_call_new_voice_only(out: *nx_call, audio: *nx_audio_session) -> i64 { 77 if audio as i64 == 0 { return -1 } 78 out.call_id = _nx_call_next_id() 79 out.remote_peer = 0 80 out.started_ms = 0 81 out.state = NX_CALL_STATE_IDLE 82 out.audio = audio 83 out.chat = 0 as *nx_chat_session 84 return 0 85} 86 87func nx_call_new_voice_chat(out: *nx_call, 88 audio: *nx_audio_session, 89 chat: *nx_chat_session) -> i64 { 90 if audio as i64 == 0 { return -1 } 91 if chat as i64 == 0 { return -1 } 92 out.call_id = _nx_call_next_id() 93 out.remote_peer = 0 94 out.started_ms = 0 95 out.state = NX_CALL_STATE_IDLE 96 out.audio = audio 97 out.chat = chat 98 return 0 99} 100 101// ---- Lifecycle ---- 102// 103// open()/close() compose both sessions. If audio fails to open, the 104// call goes to FAILED state. Chat open failure is non-fatal -- voice 105// keeps working (per privacy doctrine: chat is enhancement, voice is 106// the load-bearing channel for a call). 107 108func nx_call_open(c: *nx_call, now_unix_ms: i64) -> i64 { 109 let amss: *nx_media_session = c.audio as *nx_media_session 110 let arc: i64 = nx_media_session_open(amss) 111 if arc != 0 { 112 c.state = NX_CALL_STATE_FAILED 113 return arc 114 } 115 if c.chat as i64 != 0 { 116 let cmss: *nx_media_session = c.chat as *nx_media_session 117 let crc: i64 = nx_media_session_open(cmss) 118 // Chat failure is non-fatal -- silently drop chat from this call. 119 if crc != 0 { 120 c.chat = 0 as *nx_chat_session 121 } 122 } 123 c.state = NX_CALL_STATE_LIVE 124 c.started_ms = now_unix_ms 125 return 0 126} 127 128func nx_call_close(c: *nx_call) -> i64 { 129 if c.audio as i64 != 0 { 130 let amss: *nx_media_session = c.audio as *nx_media_session 131 nx_media_session_close(amss) 132 } 133 if c.chat as i64 != 0 { 134 let cmss: *nx_media_session = c.chat as *nx_media_session 135 nx_media_session_close(cmss) 136 } 137 c.state = NX_CALL_STATE_ENDED 138 return 0 139} 140 141// ---- Per-peer ---- 142 143func nx_call_on_remote_join(c: *nx_call, peer_id: i64, 144 reason: i64, now_unix_ms: i64) -> i64 { 145 c.remote_peer = peer_id 146 if c.audio as i64 != 0 { 147 let amss: *nx_media_session = c.audio as *nx_media_session 148 nx_media_session_on_peer_join(amss, peer_id, reason) 149 } 150 if c.chat as i64 != 0 { 151 let cmss: *nx_media_session = c.chat as *nx_media_session 152 nx_media_session_on_peer_join(cmss, peer_id, reason) 153 } 154 return 0 155} 156 157func nx_call_on_remote_leave(c: *nx_call, reason: i64) -> i64 { 158 let peer: i64 = c.remote_peer 159 if c.audio as i64 != 0 { 160 let amss: *nx_media_session = c.audio as *nx_media_session 161 nx_media_session_on_peer_leave(amss, peer, reason) 162 } 163 if c.chat as i64 != 0 { 164 let cmss: *nx_media_session = c.chat as *nx_media_session 165 nx_media_session_on_peer_leave(cmss, peer, reason) 166 } 167 c.remote_peer = 0 168 return 0 169} 170 171// ---- Frame I/O passthroughs (so consumer can drive directly) ---- 172 173func nx_call_capture_encode(c: *nx_call, 174 wire_out: *u8, wire_cap: i64) -> i64 { 175 return nx_audio_session_capture_encode(c.audio, wire_out, wire_cap) 176} 177 178func nx_call_decode_play(c: *nx_call, wire_in: *u8, wire_n: i64) -> i64 { 179 return nx_audio_session_decode_play(c.audio, wire_in, wire_n) 180} 181 182func nx_call_send_text(c: *nx_call, peer_id: i64, 183 txt: *u8, txt_len: i64, unix_ms: i64) -> i64 { 184 if c.chat as i64 == 0 { return -1 } 185 return nx_chat_session_post(c.chat, peer_id, NX_CHAT_KIND_TEXT, txt, txt_len, unix_ms) 186} 187 188// ---- Accessors ---- 189 190func nx_call_id(c: *nx_call) -> i64 { return c.call_id } 191func nx_call_state(c: *nx_call) -> i64 { return c.state } 192func nx_call_remote_peer(c: *nx_call) -> i64 { return c.remote_peer } 193func nx_call_uptime_ms(c: *nx_call, now_unix_ms: i64) -> i64 { 194 if c.started_ms == 0 { return 0 } 195 return now_unix_ms - c.started_ms 196} 197func nx_call_audio(c: *nx_call) -> *nx_audio_session { return c.audio } 198func nx_call_chat(c: *nx_call) -> *nx_chat_session { return c.chat } 199func nx_call_has_chat(c: *nx_call) -> i64 { 200 if c.chat as i64 == 0 { return 0 } 201 return 1 202} 203 204// ---- Health check (composes both session health verdicts) ---- 205 206func nx_call_health_check(c: *nx_call) -> i64 { 207 let amss: *nx_media_session = c.audio as *nx_media_session 208 let av: i64 = nx_media_session_health_check(amss) 209 if av != NX_MEDIA_HEALTH_OK { return av } 210 if c.chat as i64 != 0 { 211 let cmss: *nx_media_session = c.chat as *nx_media_session 212 let cv: i64 = nx_media_session_health_check(cmss) 213 // Chat degradation downgrades to DEGRADED but never FAILING -- 214 // voice is load-bearing. 215 if cv != NX_MEDIA_HEALTH_OK { return NX_MEDIA_HEALTH_DEGRADED } 216 } 217 return NX_MEDIA_HEALTH_OK 218}