ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
discovery/main.c
Go to the documentation of this file.
1
34#include "main.h"
35#include "../main.h" // Global exit API
36#include "session.h"
37#include <ascii-chat/session/capture.h>
38#include <ascii-chat/session/display.h>
39#include <ascii-chat/session/host.h>
40#include <ascii-chat/session/render.h>
41#include <ascii-chat/session/keyboard_handler.h>
42#include <ascii-chat/session/client_like.h>
43
44#include <stdio.h>
45
46#include <ascii-chat/log/logging.h>
47#include <ascii-chat/options/options.h>
48#include <ascii-chat/options/common.h>
49#include <ascii-chat/util/time.h>
50#include <ascii-chat/platform/abstraction.h>
51#include <ascii-chat/platform/keyboard.h>
52
53// Global exit API from src/main.c
54extern bool should_exit(void);
55
56/* ============================================================================
57 * Global Discovery Session
58 * ============================================================================ */
59
61static discovery_session_t *g_discovery = NULL;
62
63/* ============================================================================
64 * State Change Callback
65 * ============================================================================ */
66
73static void on_discovery_state_change(discovery_state_t new_state, void *user_data) {
74 (void)user_data; // Unused
75
76 const char *state_names[] = {"INIT", "CONNECTING_ACDS", "CREATING_SESSION", "JOINING_SESSION",
77 "WAITING_PEER", "NEGOTIATING", "STARTING_HOST", "CONNECTING_HOST",
78 "ACTIVE", "MIGRATING", "FAILED", "ENDED"};
79
80 if (new_state >= 0 && new_state < (int)(sizeof(state_names) / sizeof(state_names[0]))) {
81 log_info("Discovery state: %s", state_names[new_state]);
82 }
83}
84
91static void on_session_ready(const char *session_string, void *user_data) {
92 (void)user_data; // Unused
93
94 if (session_string && session_string[0]) {
95 log_info("Session ready! Share this with your peer: %s", session_string);
96 }
97}
98
106static void on_discovery_error(asciichat_error_t error, const char *message, void *user_data) {
107 (void)user_data; // Unused
108
109 log_error("Discovery error (%d): %s", error, message ? message : "Unknown");
110 signal_exit();
111}
112
122static bool discovery_participant_render_should_exit(void *user_data) {
123 discovery_session_t *discovery = (discovery_session_t *)user_data;
124
125 // Check global exit flag first
126 if (should_exit()) {
127 return true;
128 }
129
130 // Process discovery session events (keep NAT negotiation responsive)
131 // 50ms timeout for responsiveness
132 asciichat_error_t result = discovery_session_process(discovery, 50 * NS_PER_MS_INT);
133 if (result != ASCIICHAT_OK && result != ERROR_NETWORK_TIMEOUT) {
134 log_error("Discovery session process failed: %d", result);
135 signal_exit();
136 return true;
137 }
138
139 // Exit if session is no longer active
140 if (!discovery_session_is_active(discovery)) {
141 // Still negotiating - continue loop
142 return false;
143 }
144
145 return false;
146}
147
155static bool discovery_capture_should_exit_adapter(void *user_data) {
156 (void)user_data; // Unused parameter
157 return should_exit();
158}
159
167static bool discovery_display_should_exit_adapter(void *user_data) {
168 (void)user_data; // Unused parameter
169 return should_exit();
170}
171
182static void discovery_keyboard_handler(session_capture_ctx_t *capture, int key, void *user_data) {
183 (void)user_data; // Unused parameter
184 // Discovery mode doesn't have a display context yet, pass NULL for display
185 // Help screen will be silently ignored (no-op)
186 session_handle_keyboard_input(capture, NULL, (keyboard_key_t)key);
187}
188
189/* ============================================================================
190 * Discovery Run Callback (for session_client_like)
191 * ============================================================================ */
192
204static asciichat_error_t discovery_run(session_capture_ctx_t *capture, session_display_ctx_t *display,
205 void *user_data) {
206 (void)user_data; // Unused
207
208 asciichat_error_t result = ASCIICHAT_OK;
209
210 // Wait for session to become active (host negotiation complete)
211 // This processes ACDS events until we have a determined role
212 while (!should_exit()) {
213 result = discovery_session_process(g_discovery, 50 * NS_PER_MS_INT);
214 if (result != ASCIICHAT_OK && result != ERROR_NETWORK_TIMEOUT) {
215 log_error("Discovery session process failed: %d", result);
216 return result;
217 }
218
219 if (discovery_session_is_active(g_discovery)) {
220 break; // Session is active, role is determined
221 }
222 }
223
224 if (should_exit()) {
225 log_info("Shutdown requested during discovery negotiation");
226 return ASCIICHAT_OK;
227 }
228
229 // Session is active - run based on our role
230 bool we_are_host = discovery_session_is_host(g_discovery);
231
232 if (we_are_host) {
233 // HOST ROLE: Capture own media, manage participants, mix and broadcast
234 log_info("Hosting session - capturing and broadcasting");
235
236 // Create session host for managing participants
237 session_host_config_t host_config = {
238 .port = (int)GET_OPTION(port),
239 .ipv4_address = NULL, // Bind to any
240 .ipv6_address = NULL,
241 .max_clients = 32,
242 .encryption_enabled = false, // TODO: Add crypto support
243 .key_path = NULL,
244 .password = NULL,
245 .callbacks = {0},
246 .user_data = NULL,
247 };
248
249 session_host_t *host = session_host_create(&host_config);
250 if (!host) {
251 log_fatal("Failed to create session host");
252 return ERROR_MEMORY;
253 }
254
255 // Start host (accept connections, render threads)
256 result = session_host_start(host);
257 if (result != ASCIICHAT_OK) {
258 log_error("Failed to start session host: %d", result);
260 return result;
261 }
262
263 // Add memory participant for host's own media
264 uint32_t host_participant_id = session_host_add_memory_participant(host);
265 if (host_participant_id == 0) {
266 log_error("Failed to add memory participant for host");
267 session_host_stop(host);
269 return ERROR_INVALID_STATE;
270 }
271
272 log_info("Host participating with ID %u", host_participant_id);
273
274 // Main loop: capture own media and keep discovery responsive
275 while (!should_exit() && discovery_session_is_active(g_discovery)) {
276 // Capture frame from webcam (reuse existing capture context)
277 image_t *frame = session_capture_read_frame(capture);
278 if (frame) {
279 // Inject host's frame into mixer
280 session_host_inject_frame(host, host_participant_id, frame);
281 }
282
283 // Keep discovery session responsive (NAT negotiations, migrations)
284 result = discovery_session_process(g_discovery, 10 * NS_PER_MS_INT);
285 if (result != ASCIICHAT_OK && result != ERROR_NETWORK_TIMEOUT) {
286 log_error("Discovery session process failed: %d", result);
287 break;
288 }
289
290 // Frame rate limiting (60 FPS)
292 }
293
294 // Cleanup
295 session_host_stop(host);
297
298 if (should_exit()) {
299 return ASCIICHAT_OK;
300 }
301
302 // If we reach here, discovery session is no longer active (role change)
303 // Return error to trigger potential reconnection retry
304 if (!discovery_session_is_active(g_discovery)) {
305 log_info("Session ended or role changed");
306 return ASCIICHAT_OK;
307 }
308 } else {
309 // PARTICIPANT ROLE: Capture local media and display host's frames
310 log_info("Participant in session - displaying host's frames");
311
312 // Use the unified render loop that handles capture, ASCII conversion, and display
313 result = session_render_loop(capture, display, discovery_participant_render_should_exit,
314 NULL, // No custom capture callback
315 NULL, // No custom sleep callback
316 discovery_keyboard_handler, // Keyboard handler for interactive controls
317 g_discovery); // Pass discovery session as user_data
318
319 if (result != ASCIICHAT_OK) {
320 log_error("Render loop failed with error code: %d", result);
321 }
322 }
323
324 return result;
325}
326
327/* ============================================================================
328 * Main Discovery Mode Loop
329 * ============================================================================ */
330
338int discovery_main(void) {
339 log_debug("discovery_main() starting");
340
341 // Discovery-specific setup: initialize ACDS connection and session negotiation
342 // Note: Shared setup (keepawake, splash, terminal, capture, display, audio)
343 // is handled by session_client_like_run()
344
345 // Get session string from options (command-line argument)
346 const char *session_string = GET_OPTION(session_string);
347 bool is_initiator = (session_string == NULL || session_string[0] == '\0');
348
349 int port_int = GET_OPTION(port);
350
351 log_debug("Discovery: is_initiator=%d, port=%d", is_initiator, port_int);
352
353 // Create discovery session configuration
354 discovery_config_t discovery_config = {
355 .acds_address = GET_OPTION(discovery_server),
356 .acds_port = (uint16_t)GET_OPTION(discovery_port),
357 .session_string = is_initiator ? NULL : session_string,
358 .local_port = (uint16_t)port_int,
359 .on_state_change = on_discovery_state_change,
360 .on_session_ready = on_session_ready,
361 .on_error = on_discovery_error,
362 .callback_user_data = NULL,
363 .should_exit_callback = discovery_capture_should_exit_adapter,
364 .exit_callback_data = NULL,
365 };
366
367 // Create discovery session (stored in global for discovery_run() to access)
368 g_discovery = discovery_session_create(&discovery_config);
369 if (!g_discovery) {
370 log_fatal("Failed to create discovery session");
371 return ERROR_MEMORY;
372 }
373
374 // Start discovery session (connects to ACDS, creates/joins, initiates NAT negotiation)
375 log_debug("Discovery: starting discovery session");
376 asciichat_error_t result = discovery_session_start(g_discovery);
377 if (result != ASCIICHAT_OK) {
378 log_fatal("Failed to start discovery session: %d", result);
379 discovery_session_destroy(g_discovery);
380 g_discovery = NULL;
381 return result;
382 }
383
384 // No network interrupt callback needed - discovery session handles its own shutdown
386
387 /* ========================================================================
388 * Configure and Run Shared Client-Like Session Framework
389 * ========================================================================
390 * session_client_like_run() handles all shared initialization:
391 * - Terminal output management
392 * - Keepawake system
393 * - Splash screen lifecycle
394 * - Media source selection
395 * - FPS probing
396 * - Audio initialization
397 * - Display context creation
398 * - Proper cleanup ordering
399 *
400 * Discovery mode provides:
401 * - discovery_run() callback: NAT negotiation, role determination, media handling
402 * - discovery_keyboard_handler: interactive controls for participant role
403 */
404
405 session_client_like_config_t config = {
406 .run_fn = discovery_run,
407 .run_user_data = NULL,
408 .tcp_client = NULL,
409 .websocket_client = NULL,
410 .discovery = (void *)g_discovery, // Opaque pointer to discovery session
411 .custom_should_exit = NULL,
412 .exit_user_data = NULL,
413 .keyboard_handler = discovery_keyboard_handler,
414 .max_reconnect_attempts = 0, // Discovery doesn't retry - role is determined once
415 .should_reconnect_callback = NULL,
416 .reconnect_user_data = NULL,
417 .reconnect_delay_ms = 0,
418 .print_newline_on_tty_exit = false, // Server/participant manages cursor
419 };
420
421 log_debug("Discovery: calling session_client_like_run()");
422 asciichat_error_t session_result = session_client_like_run(&config);
423 log_debug("Discovery: session_client_like_run() returned %d", session_result);
424
425 // Cleanup discovery session
426 log_debug("Discovery: cleaning up");
427
428 if (g_discovery) {
429 discovery_session_stop(g_discovery);
430 discovery_session_destroy(g_discovery);
431 g_discovery = NULL;
432 }
433
434 return (session_result == ASCIICHAT_OK) ? 0 : (int)session_result;
435}
asciichat_error_t session_client_like_run(const session_client_like_config_t *config)
Definition client_like.c:96
bool should_exit(void)
Definition main.c:90
int discovery_main(void)
Run discovery mode main loop.
asciichat_error_t session_host_inject_frame(session_host_t *host, uint32_t participant_id, const image_t *frame)
Definition host.c:1072
uint32_t session_host_add_memory_participant(session_host_t *host)
Definition host.c:996
void session_host_stop(session_host_t *host)
Definition host.c:857
asciichat_error_t session_host_start(session_host_t *host)
Definition host.c:797
void session_host_destroy(session_host_t *host)
Definition host.c:222
session_host_t * session_host_create(const session_host_config_t *config)
Definition host.c:166
void session_handle_keyboard_input(session_capture_ctx_t *capture, session_display_ctx_t *display, keyboard_key_t key)
image_t * session_capture_read_frame(session_capture_ctx_t *ctx)
void session_capture_sleep_for_fps(session_capture_ctx_t *ctx)
asciichat_error_t session_render_loop(session_capture_ctx_t *capture, session_display_ctx_t *display, session_should_exit_fn should_exit, session_capture_fn capture_cb, session_sleep_for_frame_fn sleep_cb, session_keyboard_handler_fn keyboard_handler, void *user_data)
void signal_exit(void)
Definition main.c:94
void set_interrupt_callback(void(*cb)(void))
Definition main.c:102
ascii-chat Server Mode Entry Point Header
Discovery session flow management.
discovery_state_t
Discovery session state.
Definition session.h:116
bool discovery_session_is_active(const discovery_session_t *session)
Check if session is active (call in progress)
void discovery_session_stop(discovery_session_t *session)
Stop the discovery session.
bool discovery_session_is_host(const discovery_session_t *session)
Check if we are the host.
discovery_session_t * discovery_session_create(const discovery_config_t *config)
Create a new discovery session.
asciichat_error_t discovery_session_start(discovery_session_t *session)
Start the discovery session.
asciichat_error_t discovery_session_process(discovery_session_t *session, int64_t timeout_ns)
Process session events (call in main loop)
void discovery_session_destroy(discovery_session_t *session)
Destroy discovery session and free resources.
Configuration for discovery session.
Definition session.h:211
const char * acds_address
ACDS address (default: "127.0.0.1")
Definition session.h:213
Discovery session context.
Definition session.h:134
Internal session display context structure.
Internal session host structure.
Definition host.c:82