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}