ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
client_like.c
Go to the documentation of this file.
1
8#include "session/capture.h"
9#include "session/display.h"
10#include "session/render.h"
12#include "../../client/audio.h"
13
21
22#include <ascii-chat/log/log.h>
26#include <ascii-chat/util/url.h>
28
29#include <string.h>
30#include <ascii-chat/atomic.h>
31
32// Forward declarations (implemented below)
33static bool capture_should_exit_adapter(void *user_data);
34static bool display_should_exit_adapter(void *user_data);
35static bool session_client_like_is_networked(session_client_like_kind_t kind);
36
37static asciichat_error_t validate_mirror_webcam_selection(const session_client_like_config_t *config) {
38 if (!config || config->kind != SESSION_CLIENT_LIKE_KIND_MIRROR || !terminal_is_stdout_tty() ||
39 GET_OPTION(test_pattern)) {
40 return ASCIICHAT_OK;
41 }
42
43 const char *media_file = GET_OPTION(media_file);
44 const char *media_url = GET_OPTION(media_url);
45 if ((media_file && media_file[0] != '\0') || (media_url && media_url[0] != '\0')) {
46 return ASCIICHAT_OK;
47 }
48
49 webcam_device_info_t *devices = NULL;
50 unsigned int device_count = 0;
51 asciichat_error_t result = webcam_list_devices(&devices, &device_count);
52 if (result != ASCIICHAT_OK) {
53 return SET_ERRNO(ERROR_WEBCAM, "Unable to enumerate webcams before starting mirror mode");
54 }
55
56 unsigned int selected_index = (unsigned int)GET_OPTION(webcam_index);
57 if (selected_index >= device_count) {
58 if (device_count == 0) {
59 result =
60 SET_ERRNO(ERROR_WEBCAM, "Webcam index %u is unavailable: no webcam devices were found (run --list-webcams)",
61 selected_index);
62 } else {
63 result = SET_ERRNO(ERROR_WEBCAM,
64 "Webcam index %u is unavailable: %u webcam device(s) found; valid indices are 0-%u "
65 "(run --list-webcams)",
66 selected_index, device_count, device_count - 1);
67 }
68 } else {
69 result = ASCIICHAT_OK;
70 }
71
73 return result;
74}
75
76// Module-level config for adapter callbacks
77static const session_client_like_config_t *g_current_config = NULL;
78
79// Module-level adapter functions for render loop access
80static bool (*g_render_should_exit)(void *) = NULL;
81
82// Module-level stdin reader for ASCII-to-video rendering (stdin render mode only)
83static terminal_fd_reader_t *g_stdin_reader = NULL;
84
85/* ============================================================================
86 * Public Accessors
87 * ============================================================================ */
88
90 return g_render_should_exit;
91}
92
94 return g_stdin_reader;
95}
96
97/* ============================================================================
98 * Exit Condition Adapters
99 * ============================================================================ */
100
105static bool capture_should_exit_adapter(void *user_data) {
106 (void)user_data;
108 return true;
109 }
110 if (g_current_config && g_current_config->custom_should_exit) {
111 return g_current_config->custom_should_exit(g_current_config->exit_user_data);
112 }
113 return false;
114}
115
120static bool display_should_exit_adapter(void *user_data) {
121 (void)user_data;
123 return true;
124 }
125 if (g_current_config && g_current_config->custom_should_exit) {
126 return g_current_config->custom_should_exit(g_current_config->exit_user_data);
127 }
128 return false;
129}
130
131static bool session_client_like_is_networked(session_client_like_kind_t kind) {
133}
134
136 log_info("session_client_like_run: ENTRY");
137 if (!config || !config->run_fn) {
138 return SET_ERRNO(ERROR_INVALID_PARAM, "config or run_fn is NULL");
139 }
140 log_info("session_client_like_run: config validated");
141
142 // Store config globally for adapter callbacks
143 g_current_config = config;
144
145 // Store the render should_exit adapter globally so mode-specific run_fn can access it
146 g_render_should_exit = display_should_exit_adapter;
147
149
150 // Keep track of what's been initialized for cleanup
151 session_capture_ctx_t *capture = NULL;
152 session_display_ctx_t *display = NULL;
153 audio_context_t *audio_ctx = NULL;
154 media_source_t *probe_source = NULL;
155 bool audio_available = false;
156
157 // ============================================================================
158 // SETUP: Terminal and Logging
159 // ============================================================================
160
161 log_debug("session_client_like_run(): Setting up terminal and logging");
162
163 // Force stderr when stdout is piped (prevents ASCII corruption in output stream)
164 bool should_force_stderr = terminal_should_force_stderr();
165 log_debug("terminal_should_force_stderr()=%d", should_force_stderr);
166
167 if (should_force_stderr) {
168 // Redirect logs to stderr to prevent corruption of stdout (for pipes)
169 // But keep terminal output enabled so we can see initialization errors
171 }
172
173 // Note: Force all logging to stderr if encoding to pipe to prevent corrupting binary video output
174 // (implemented via should_force_stderr check above)
175 const char *render_file_early = GET_OPTION(render_file);
176 log_info("[CLIENT_LIKE_RUN] render_file_early=%s (ptr=%p)", render_file_early ? render_file_early : "(null)",
177 (void *)render_file_early);
178 if (render_file_early && render_file_early[0] != '\0' &&
179 (strcmp(render_file_early, "-") == 0 || strcmp(render_file_early, "pipe:") == 0)) {
180 log_info("--render-file=- will output binary video to stdout, logging sent to stderr");
181 }
182
183 // ============================================================================
184 // SETUP: Keepawake System
185 // ============================================================================
186
187 log_debug("session_client_like_run(): Validating keepawake options");
188
189 // Validate mutual exclusivity
190 bool enable_ka = GET_OPTION(enable_keepawake);
191 bool disable_ka = GET_OPTION(disable_keepawake);
192 log_debug("enable_keepawake=%d, disable_keepawake=%d", enable_ka, disable_ka);
193
194 if (enable_ka && disable_ka) {
195 result = SET_ERRNO(ERROR_INVALID_PARAM, "--keepawake and --no-keepawake are mutually exclusive");
196 goto cleanup;
197 }
198
199 log_debug("session_client_like_run(): Enabling keepawake if needed");
200
201 // Enable keepawake unless explicitly disabled
202 if (!disable_ka) {
204 }
205
206 log_debug("session_client_like_run(): Keepawake setup complete");
207
208 result = validate_mirror_webcam_selection(config);
209 if (result != ASCIICHAT_OK) {
210 goto cleanup;
211 }
212
213 // ============================================================================
214 // SETUP: Display Context (before splash for unified TTY initialization)
215 // ============================================================================
216
217 // Create display context early to share TTY initialization between splash and rendering
218 // This prevents double TTY init which was corrupting the terminal state
219 session_display_config_t display_config = {0};
220 display_config.snapshot_mode = GET_OPTION(snapshot_mode);
221 display_config.palette_type = GET_OPTION(palette_type);
222 display_config.custom_palette = GET_OPTION(palette_custom_set) ? GET_OPTION(palette_custom) : NULL;
223 display_config.color_mode = TERM_COLOR_AUTO;
224 display_config.should_exit_callback = display_should_exit_adapter;
225 display_config.callback_data = NULL;
226 display_config.defer_render_file = true;
227
228 log_debug("[SETUP_DISPLAY_EARLY] Creating display context (before splash)");
229 display = session_display_create(&display_config);
230 if (!display) {
231 log_debug("[SETUP_DISPLAY_EARLY] session_display_create() returned NULL");
232 log_fatal("Failed to initialize display context");
233 result = ERROR_DISPLAY;
234 goto cleanup;
235 }
236 log_debug("[SETUP_DISPLAY_EARLY] Display context created, will use for splash");
237
238 // Register global display context for signal handlers (Ctrl+C help screen closure)
239 // This makes the context available to signal handlers for the entire app lifetime
241
242 // ============================================================================
243 // SETUP: Splash Screen (using unified display context)
244 // ============================================================================
245
246 log_debug("session_client_like_run(): Starting splash with display context");
247
248 // Use the display context we just created for splash animation
249 splash_intro_start(display);
250 log_debug("session_client_like_run(): splash_intro_start() returned");
251 if (splash_is_running()) {
253 }
254
255 // Detect if we're using media vs webcam (needed for splash timing)
256 const char *media_url = GET_OPTION(media_url);
257 const char *media_file = GET_OPTION(media_file);
258 bool has_media = (media_url && strlen(media_url) > 0) || (media_file && strlen(media_file) > 0);
259
260 // Show splash briefly for webcam, but skip sleep for media (which takes time anyway)
261 if (!has_media && !GET_OPTION(snapshot_mode)) {
262 APP_CALLBACK_VOID(platform_pump_events);
264 }
265 log_debug("session_client_like_run(): After splash sleep");
266
267 // Restore stderr now that splash animation and post-splash logging are done
268 // This allows logs to appear on screen again after this point
270
271 // Wait for background update check to finish and show prompt if update available
273 if (update_banner_has_update() && !GET_OPTION(snapshot_mode)) {
274 bool wants_update = update_banner_show_prompt(display);
275 if (wants_update) {
277 return ASCIICHAT_OK;
278 }
280 }
281
282 // ============================================================================
283 // SETUP: Terminal Logging in Snapshot Mode
284 // ============================================================================
285
286 log_debug("session_client_like_run(): About to disable terminal logging (snapshot=%d, discovery=%d)",
287 GET_OPTION(snapshot_mode), config->discovery != NULL);
288 // For discovery mode, skip disabling logging initially - we'll handle it after role is determined
289 // Discovery participants should show logs, only hosts should have logs disabled for rendering
290 bool should_disable_logging = !GET_OPTION(snapshot_mode) && (config->discovery == NULL);
291 if (should_disable_logging) {
292 log_debug("session_client_like_run(): Calling log_set_terminal_output(false)...");
294 log_debug("session_client_like_run(): RETURNED from log_set_terminal_output(false)");
295 } else {
296 log_debug("session_client_like_run(): Skipping log disable (snapshot=%d or discovery=%d)",
297 GET_OPTION(snapshot_mode), config->discovery != NULL);
298 }
299 log_debug("session_client_like_run(): Terminal logging setup complete, starting media source setup");
300
301 // ============================================================================
302 // SETUP: Media Source Selection and FPS Probing
303 // ============================================================================
304
305 log_debug("session_client_like_run(): Initializing capture config");
306 session_capture_config_t capture_config = {0};
308 capture_config.should_exit_callback = capture_should_exit_adapter;
309 capture_config.callback_data = NULL;
310
311 // Determine FPS explicitly set by user
312 log_debug("session_client_like_run(): Getting FPS option");
313 int user_fps = GET_OPTION(fps);
314 bool fps_explicitly_set = GET_OPTION(fps_explicitly_set);
315 log_debug("session_client_like_run(): FPS=%d (explicitly_set=%d)", user_fps, fps_explicitly_set);
316
317 // Select media source based on options (priority order)
318 log_debug("session_client_like_run(): Getting media_url option");
319 const char *media_url_val = GET_OPTION(media_url);
320 log_debug("session_client_like_run(): Getting media_file option");
321 const char *media_file_val = GET_OPTION(media_file);
322 log_debug("session_client_like_run(): media_url=%s, media_file=%s", media_url_val ? media_url_val : "(null)",
323 media_file_val ? media_file_val : "(null)");
324
325 if (media_url_val && strlen(media_url_val) > 0) {
326 // Network URL takes priority
327 log_info("Using network URL: %s (webcam disabled)", media_url_val);
328 capture_config.type = MEDIA_SOURCE_FILE;
329 capture_config.path = media_url_val;
330 capture_config.loop = false; // Network URLs cannot be looped
331
332 if (fps_explicitly_set) {
333 capture_config.target_fps = (uint32_t)user_fps;
334 log_info("Using user-specified FPS: %u", capture_config.target_fps);
335 } else {
336 // Always probe FPS for HTTP URLs, even in snapshot mode
337 // (snapshot mode needs correct FPS to capture right number of frames during snapshot_delay window)
338 log_debug("Probing FPS for HTTP URL");
339 probe_source = media_source_create(MEDIA_SOURCE_FILE, media_url_val);
340 if (probe_source) {
341 double url_fps = media_source_get_video_fps(probe_source);
342 log_info("Detected HTTP stream video FPS: %.1f", url_fps);
343 if (url_fps > 0.0) {
344 capture_config.target_fps = (uint32_t)(url_fps + 0.5);
345 } else {
346 log_warn("FPS detection failed for HTTP stream, using default 60 FPS");
347 capture_config.target_fps = 60;
348 }
349 } else {
350 log_warn("Failed to create probe source for HTTP stream, using default 60 FPS");
351 capture_config.target_fps = 60;
352 }
353 }
354 } else if (media_file_val && strlen(media_file_val) > 0) {
355 // Local file or stdin
356 log_info("DEBUG: media_file_val='%s', strcmp result=%d", media_file_val, strcmp(media_file_val, "-"));
357 if (strcmp(media_file_val, "-") == 0) {
358 log_info("Using stdin for media streaming (webcam disabled)");
359 capture_config.type = MEDIA_SOURCE_STDIN;
360 capture_config.path = NULL;
361 capture_config.loop = false;
362 if (fps_explicitly_set) {
363 capture_config.target_fps = (uint32_t)user_fps;
364 } else {
365 log_debug("Probing FPS from buffered stdin media");
366 probe_source = media_source_create(MEDIA_SOURCE_STDIN, NULL);
367 if (probe_source) {
368 double stdin_fps = media_source_get_video_fps(probe_source);
369 log_info("Detected stdin video FPS: %.1f", stdin_fps);
370 capture_config.target_fps = stdin_fps > 0.0 ? (uint32_t)(stdin_fps + 0.5) : 60;
371 } else {
372 log_warn("Failed to probe stdin FPS, using default 60 FPS");
373 capture_config.target_fps = 60;
374 }
375 }
376 } else {
377 log_info("Using media file: %s (webcam disabled)", media_file_val);
378 capture_config.type = MEDIA_SOURCE_FILE;
379 capture_config.path = media_file_val;
380 capture_config.loop = GET_OPTION(media_loop);
381
382 if (fps_explicitly_set) {
383 capture_config.target_fps = (uint32_t)user_fps;
384 log_info("Using user-specified FPS: %u", capture_config.target_fps);
385 } else {
386 // Always probe FPS for local files, even in snapshot mode
387 // (snapshot mode needs correct FPS to capture right number of frames during snapshot_delay window)
388 log_debug("Probing FPS for local file");
389 probe_source = media_source_create(MEDIA_SOURCE_FILE, media_file_val);
390 if (probe_source) {
391 double file_fps = media_source_get_video_fps(probe_source);
392 log_info("Detected file video FPS: %.1f", file_fps);
393 if (file_fps > 0.0) {
394 capture_config.target_fps = (uint32_t)(file_fps + 0.5);
395 } else {
396 log_warn("FPS detection failed, using default 60 FPS");
397 capture_config.target_fps = 60;
398 }
399 } else {
400 log_warn("Failed to create probe source for FPS detection, using default 60 FPS");
401 capture_config.target_fps = 60;
402 }
403 }
404 }
405 } else if (GET_OPTION(test_pattern)) {
406 // Test pattern
407 log_debug("session_client_like_run(): Using test pattern");
408 log_info("Using test pattern");
409 capture_config.type = MEDIA_SOURCE_TEST;
410 capture_config.path = NULL;
411 capture_config.target_fps = fps_explicitly_set ? (uint32_t)user_fps : 60;
412 capture_config.loop = false;
413 } else {
414 // Default: Try webcam if stdout is a TTY, otherwise use test pattern
415 // (webcam + piped output often fails, test pattern is reliable fallback)
416 bool stdout_is_tty = terminal_is_stdout_tty();
417 if (stdout_is_tty) {
418 // Normal terminal mode: use webcam
419 log_debug("session_client_like_run(): Stdout is TTY, using local webcam");
420 log_info("Using local webcam");
421 capture_config.type = MEDIA_SOURCE_WEBCAM;
422 static char webcam_index_str[32];
423 safe_snprintf(webcam_index_str, sizeof(webcam_index_str), "%u", GET_OPTION(webcam_index));
424 capture_config.path = webcam_index_str;
425 log_info("Using webcam device index %u", GET_OPTION(webcam_index));
426 capture_config.target_fps = fps_explicitly_set ? (uint32_t)user_fps : 60;
427 capture_config.loop = false;
428 } else {
429 // Piped output mode: use test pattern (webcam + pipes often fails)
430 log_debug("session_client_like_run(): Stdout is piped, defaulting to test pattern for reliability");
431 log_info("Stdout is piped - using test pattern instead of webcam");
432 capture_config.type = MEDIA_SOURCE_TEST;
433 capture_config.path = NULL;
434 capture_config.target_fps = fps_explicitly_set ? (uint32_t)user_fps : 60;
435 capture_config.loop = false;
436 }
437 }
438
439 // Apply initial seek if specified
440 log_debug("session_client_like_run(): Getting media_seek_timestamp option");
441 capture_config.initial_seek_timestamp = GET_OPTION(media_seek_timestamp);
442 log_debug("session_client_like_run(): Media seek timestamp set");
443
444 // Clean up probe source (don't reuse for actual capture)
445 if (probe_source) {
446 log_debug("session_client_like_run(): Destroying probe source");
447 media_source_destroy(probe_source);
448 probe_source = NULL;
449 log_debug("session_client_like_run(): Probe source destroyed");
450 } else {
451 log_debug("session_client_like_run(): No probe source to destroy");
452 }
453
454 // Pass probed FPS to display context for correct render-file duration
455 if (capture_config.target_fps > 0) {
456 session_display_set_render_fps(display, capture_config.target_fps);
457 log_debug("session_client_like_run(): Set display render FPS to %u for correct video encoding duration",
458 capture_config.target_fps);
459 }
460
461 asciichat_error_t render_init_result = session_display_init_render_file(display, capture_config.target_fps);
462 if (render_init_result != ASCIICHAT_OK) {
463 result = render_init_result;
464 goto cleanup;
465 }
466
467 // ============================================================================
468 // SETUP: Network Transports (TCP/WebSocket)
469 // ============================================================================
470
471 log_debug("session_client_like_run(): Setting up network transports");
472
473 bool networked_mode = session_client_like_is_networked(config->kind);
474
475 switch (config->kind) {
477 log_debug("Mirror mode detected - will use local capture with media source");
478 break;
480 log_debug("Client/Network mode detected - will use network capture without local media source");
481 break;
483 log_debug("Discovery mode detected - discovery session will manage networking");
484 break;
485 }
486
487 // ============================================================================
488 // SETUP: Capture Context
489 // ============================================================================
490
491 // Check for stdin render mode: read ASCII frames from stdin, render to video
492 // Only enable stdin render mode if:
493 // 1. render-file is "-" (output to stdout)
494 // 2. stdin is not a TTY
495 // 3. --height is explicitly specified (user must opt-in to stdin render mode)
496 // 4. NO media source is being used (not reading from file/URL/test-pattern)
497 // This prevents accidental activation when piping to other processes or using mirror mode
498 const char *render_file_opt = GET_OPTION(render_file);
499 int frame_height = GET_OPTION(height);
500 bool height_explicitly_set = (frame_height != OPT_HEIGHT_DEFAULT);
501 bool has_media_source = (media_url_val && strlen(media_url_val) > 0) ||
502 (media_file_val && strlen(media_file_val) > 0) || GET_OPTION(test_pattern);
503
504 // Enable audio capture if render-file is active and media source exists
505 if (render_file_opt && strlen(render_file_opt) > 0 && has_media_source) {
506 capture_config.enable_audio = true;
507 log_debug("Audio capture enabled for render-file encoding");
508 }
509
510 bool stdin_render_mode =
511 (render_file_opt && strcmp(render_file_opt, "-") == 0 && !terminal_is_stdin_tty() && height_explicitly_set &&
512 !has_media_source && config->kind != SESSION_CLIENT_LIKE_KIND_MIRROR);
513
514 if (stdin_render_mode) {
515 // Stdin render mode: read ASCII frames from stdin, output video to stdout
516 log_info("Stdin render mode enabled: reading ASCII frames from stdin, output to stdout");
517
518 // Get frame dimensions (--height must be explicitly set to reach here)
519 int frame_width = GET_OPTION(width);
520
521 // Validate stdin render mode constraints (--height is already validated above since height_explicitly_set is true)
522 bool width_was_explicitly_set = (frame_width != OPT_WIDTH_DEFAULT);
523
524 if (width_was_explicitly_set) {
525 result = SET_ERRNO(ERROR_USAGE, "Stdin render mode does not accept --width (auto-detected from frames).\n"
526 "Only specify: --height <rows>");
527 goto cleanup;
528 }
529
530 asciichat_error_t stdin_err = terminal_fd_reader_create(STDIN_FILENO, frame_height, &g_stdin_reader);
531 if (stdin_err != ASCIICHAT_OK) {
532 log_fatal("Failed to initialize stdin frame reader: %s", asciichat_error_string(stdin_err));
533 result = ERROR_MEDIA_INIT;
534 goto cleanup;
535 }
536
537 // For stdin render mode, create a minimal capture context for compatibility
538 int fps = GET_OPTION(fps);
539 capture = session_network_capture_create((uint32_t)(fps > 0 ? fps : 60));
540 if (!capture) {
541 log_fatal("Failed to initialize capture context for stdin rendering");
542 terminal_fd_reader_destroy(g_stdin_reader);
543 g_stdin_reader = NULL;
544 result = ERROR_MEDIA_INIT;
545 goto cleanup;
546 }
547
548 log_info("stdin render mode: reading %d-line frames from stdin, auto-detecting width", frame_height);
549 }
550
551 // Choose capture type based on mode determined earlier:
552 // - Mirror mode: needs to capture local media (webcam, file, test pattern)
553 // - Network modes (client/discovery): receive frames from network, no local capture
554 // Discovery peers may use a local media source (for example the synthetic
555 // test pattern used by headless WebRTC tests) as their outgoing stream.
556 // Keep the network-only capture for ordinary client mode, but do not discard
557 // an explicitly requested source before the discovery transport is active.
558 bool discovery_has_local_source = config->kind == SESSION_CLIENT_LIKE_KIND_DISCOVERY;
559 if (!stdin_render_mode && networked_mode && !discovery_has_local_source) {
560 // Network mode: create minimal capture context without media source
561 log_debug("Network mode detected - using network capture (no local media source)");
562 int fps = GET_OPTION(fps);
563 uint64_t net_cap_start = time_get_ns();
564 capture = session_network_capture_create((uint32_t)(fps > 0 ? fps : 60));
565 double net_cap_ms = time_ns_to_ms(time_elapsed_ns(net_cap_start, time_get_ns()));
566 log_info("★ INIT_CHECKPOINT: Network capture created in %.1f ms", net_cap_ms);
567 if (!capture) {
568 log_fatal("Failed to initialize network capture context");
569 result = ERROR_MEDIA_INIT;
570 goto cleanup;
571 }
572 if (fps > 0) {
573 log_debug("Network capture FPS set to %d from options", fps);
574 }
575 }
576
577 // ============================================================================
578 // SETUP: Audio Context
579 // ============================================================================
580
581 // Skip audio for immediate snapshots
582 bool should_init_audio = GET_OPTION(audio_enabled);
583 if (GET_OPTION(snapshot_mode) && GET_OPTION(snapshot_delay) == 0.0) {
584 should_init_audio = false;
585 log_debug("Skipping audio initialization for immediate snapshot");
586 }
587
588 // Pass stdin_reader to display if in stdin render mode
589 if (stdin_render_mode && g_stdin_reader) {
590 session_display_set_stdin_reader(display, g_stdin_reader);
591 log_debug("stdin_reader passed to display context");
592 }
593
594 // ============================================================================
595 // SETUP: End Splash Screen
596 // ============================================================================
597 log_debug("[SETUP_SPLASH] About to end splash screen");
598
599 // The splash screen will display during initialization and exit when done.
600 // The initialization process (media probing, display setup, etc.) naturally
601 // takes time, keeping the splash visible. The splash is kept alive during
602 // connection attempts (TCP and WebSocket) to prevent a blank screen during
603 // network operations. splash_intro_done() is called only after a successful
604 // connection is established.
605
606 log_debug("[SETUP_SPLASH] Splash will remain visible during connection attempts");
607
608 // Exit early if shutdown was requested (e.g., user pressed Ctrl-C)
610 log_debug("[SETUP] Shutdown requested, exiting early");
611 log_debug("[SETUP_SPLASH] Ending splash due to early shutdown request");
614 result = ASCIICHAT_OK;
615 goto cleanup;
616 }
617
618 // Wait for splash animation to finish before proceeding to render loop
619 // (splash_anim thread uses the display, must not be disrupted before cleanup)
620 // But skip for immediate snapshots (snapshot-delay=0) which need to output first frame immediately
621 // For discovery mode, keep the splash running - it stays visible while waiting for peers
622 if (config->discovery == NULL) {
624 if (!(GET_OPTION(snapshot_mode) && GET_OPTION(snapshot_delay) == 0.0)) {
626 }
627 } else {
628 log_debug("[SETUP_SPLASH] Discovery mode: keeping splash visible while waiting for peers");
629 }
630
631 // ============================================================================
632 // SETUP: Create Mirror Capture (after splash screen finishes)
633 // ============================================================================
634
635 log_info("[MIRROR_CHECK] stdin_render_mode=%d, capture=%p", stdin_render_mode, (void *)capture);
636 if (!stdin_render_mode && !capture) {
637 // Mirror mode: create capture context with local media source (NOW, after splash finishes)
638 log_info("Mirror mode detected - creating mirror capture after splash screen");
639 uint64_t mirror_cap_start = time_get_ns();
640 capture = session_mirror_capture_create(&capture_config);
641 double mirror_cap_ms = time_ns_to_ms(time_elapsed_ns(mirror_cap_start, time_get_ns()));
642 log_info("★ INIT_CHECKPOINT: Mirror capture created in %.1f ms (after splash)", mirror_cap_ms);
643 if (!capture) {
644 log_fatal("Failed to initialize mirror capture source");
645 result = ERROR_MEDIA_INIT;
646 goto cleanup;
647 }
648 }
649
650 if (should_init_audio && config->kind == SESSION_CLIENT_LIKE_KIND_MIRROR) {
652 bool file_audio = media_source_has_audio(source);
653 bool record_mic = render_file_opt && render_file_opt[0] && !file_audio;
654 if (file_audio || record_mic) {
655 audio_ctx = SAFE_CALLOC(1, sizeof(*audio_ctx), audio_context_t *);
656 if (audio_init(audio_ctx) == ASCIICHAT_OK) {
657 audio_available = true;
658 if (file_audio && GET_OPTION(audio_capture_source) != AUDIO_CAPTURE_SOURCE_MIC)
659 audio_ctx->capture_media_source = source;
660 audio_ctx->monitor_local_media = true;
661 audio_ctx->playback_only = !audio_should_enable_microphone(GET_OPTION(audio_capture_source), file_audio);
662 session_capture_set_audio_context(capture, audio_ctx);
663 media_source_set_audio_context(source, audio_ctx);
664 } else {
665 SAFE_FREE(audio_ctx);
666 }
667 }
668 }
669
670 // ============================================================================
671 // SETUP: Start Audio Playback
672 // ============================================================================
673
674 log_debug("[SETUP_AUDIO] About to check audio context for duplex");
675 if (audio_ctx && audio_available) {
676 log_debug("[SETUP_AUDIO] Starting audio duplex");
677 if (audio_start_duplex(audio_ctx) == ASCIICHAT_OK) {
678 log_info("Audio playback started");
679 } else {
680 log_warn("Failed to start audio duplex");
683 audio_destroy(audio_ctx);
684 SAFE_FREE(audio_ctx);
685 audio_ctx = NULL;
686 audio_available = false;
687 }
688 }
689
690 log_info("[MARKER] Audio setup complete at line 614");
691 log_info("[SETUP_COMPLETE] All setup complete, about to start connection loop");
692
693 // ============================================================================
694 // RUN: Mode-Specific Main Loop with Reconnection
695 // ============================================================================
696
697 // Connection/attempt loop - wraps run_fn with reconnection logic
698 int attempt = 0;
699 int max_attempts = config->max_reconnect_attempts;
700
701 log_info("[CLIENT_LIKE_LOOP] Starting connection loop, max_attempts=%d", max_attempts);
702
703 while (true) {
704 attempt++;
705
706 result = config->run_fn(capture, display, config->run_user_data);
707
708 // Exit immediately if run_fn succeeded
709 if (result == ASCIICHAT_OK) {
710 // Connection succeeded - splash screen cleanup now happens when first frame renders
711 // This ensures the splash stays visible during TCP/WebSocket/datachannel connection attempts
712
713 break;
714 }
715
716 // Check if we should attempt reconnection
717 bool should_retry = false;
718
719 if (max_attempts != 0) { // max_attempts == 0 means no retries
720 // Check custom reconnect logic if provided
721 if (config->should_reconnect_callback) {
722 should_retry = config->should_reconnect_callback(result, attempt, config->reconnect_user_data);
723 } else {
724 should_retry = true; // Default: always retry
725 }
726
727 // Check retry limits if still planning to retry
728 if (should_retry && max_attempts > 0 && attempt >= max_attempts) {
729 log_debug("Reached maximum reconnection attempts (%d), giving up", max_attempts);
730 should_retry = false;
731 }
732 }
733
734 // If not retrying or shutdown requested, exit loop
735 if (!should_retry || APP_CALLBACK_BOOL(should_exit)) {
736 break;
737 }
738
739 // Reconnection will happen - show splash screen during reconnection attempt
740 // Reset first_frame flag so splash cleanup runs on next successful connection
742
743 // splash_intro_start() handles all the logic:
744 // - If splash should display: shows splash and disables console logging
745 // - If splash shouldn't display: restores stderr and returns without changing logging state
746 splash_intro_start(display);
747
748 // Log reconnection message
749 if (max_attempts == -1) {
750 log_info("Connection failed, retrying...");
751 } else if (max_attempts > 0) {
752 log_info("Connection failed, retrying (attempt %d/%d)...", attempt + 1, max_attempts);
753 }
754
755 // Apply reconnection delay if configured
756 // Check APP_CALLBACK_BOOL(should_exit) frequently during sleep so SIGTERM can interrupt reconnection attempts
757 if (config->reconnect_delay_ms > 0) {
758 unsigned int remaining_ms = config->reconnect_delay_ms;
759 const unsigned int check_interval_ms = 100; // Check exit flag every 100ms
760
761 while (remaining_ms > 0 && !APP_CALLBACK_BOOL(should_exit)) {
762 unsigned int sleep_ms = (remaining_ms < check_interval_ms) ? remaining_ms : check_interval_ms;
763 APP_CALLBACK_VOID(platform_pump_events);
765 remaining_ms -= sleep_ms;
766 }
767 }
768
769 // Continue loop to retry
770 }
771
772 // ============================================================================
773 // CLEANUP (always runs, even on error)
774 // ============================================================================
775
776cleanup:
777 // Re-enable terminal output for shutdown logs
779
780 g_current_config = NULL;
781 g_render_should_exit = NULL;
782
783#ifndef NDEBUG
784 // Join the registry inspector before freeing the session's synchronization objects.
786#endif
787
788 // Stop audio thread before destroying audio context to prevent use-after-free
789 // The audio worker thread may still be logging when we destroy the buffer
791
792 // Stop streams before releasing the local media source.
793 if (audio_ctx) {
794 audio_stop_duplex(audio_ctx);
795 audio_destroy(audio_ctx);
796 SAFE_FREE(audio_ctx);
797 audio_ctx = NULL;
798 }
799
800 // Destroy display (check global context to see if discovery mode already destroyed it)
801 // Discovery participant mode may destroy the display early and clear the global context
803 if (display && display == current_global) {
804 // Display is still valid (not destroyed by discovery mode)
806 display = NULL;
807 } else if (display && display != current_global) {
808 // Display was already destroyed (by discovery participant mode)
809 // Just NULL it out, don't call destroy again
810 log_debug("[CLEANUP] Display was already destroyed by discovery mode, skipping destroy");
811 display = NULL;
812 }
813
814 // Destroy capture
815 if (capture) {
817 capture = NULL;
818 }
819
820 // Destroy stdin reader if active (stdin render mode)
821 if (g_stdin_reader) {
822 terminal_fd_reader_destroy(g_stdin_reader);
823 g_stdin_reader = NULL;
824 }
825
826 // Clean up probe source if still allocated (shouldn't happen, but be safe)
827 if (probe_source) {
828 media_source_destroy(probe_source);
829 probe_source = NULL;
830 }
831
832 // Stop splash animation and enforce minimum display time (even on error path)
833 // But skip completely if shutting down - don't interact with splash at all during shutdown
834 // The animation thread will exit on its own when it detects shutdown_is_requested()
835 // For discovery mode, keep the splash running - it stays visible while waiting for peers
836 if (config->discovery != NULL) {
837 log_debug("[CLEANUP] Discovery mode: keeping splash alive for peer waiting UI");
838 } else if (!APP_CALLBACK_BOOL(should_exit)) {
839 log_debug("[CLEANUP] About to call splash_intro_done()");
841 log_debug("[CLEANUP] splash_intro_done() returned");
842
843 // Wait for animation thread to exit before cleanup
844 log_debug("[CLEANUP] About to call splash_wait_for_animation()");
846 log_debug("[CLEANUP] splash_wait_for_animation() returned");
847 } else {
848 log_debug("[CLEANUP] Skipping all splash operations (shutdown in progress)");
849 // During shutdown, don't interact with splash - let animation thread exit naturally
850 // and don't wait for it (prevents blocking on signals)
851 }
852
853 // Disable keepawake (re-allow OS to sleep)
854 log_debug("[CLEANUP] Disabling keepawake");
856 log_debug("[CLEANUP] keepawake disabled");
857
858 // Write newline to separate final frame from prompt (if configured)
859 log_debug("About to check print_newline_on_tty_exit");
861 log_debug("Writing newline");
862 const char newline = '\n';
863 (void)platform_write_all(STDOUT_FILENO, &newline, 1);
864 }
865
867
868 return result;
869}
Platform abstraction layer umbrella header providing unified cross-platform API.
Application-level callbacks for library code.
#define APP_CALLBACK_BOOL(callback_name)
#define APP_CALLBACK_VOID(callback_name)
#define APP_CALLBACK_VOID_UINT(callback_name, value)
⚛️ Atomic operations abstraction layer with debug tracking
asciichat_error_t session_client_like_run(const session_client_like_config_t *config)
bool(*)(void *) session_client_like_get_render_should_exit(void)
Definition client_like.c:89
Shared initialization and teardown for client-like modes (mirror, client, discovery)
@ SESSION_CLIENT_LIKE_KIND_CLIENT
Definition client_like.h:65
@ SESSION_CLIENT_LIKE_KIND_DISCOVERY
Definition client_like.h:66
@ SESSION_CLIENT_LIKE_KIND_MIRROR
Definition client_like.h:64
enum session_client_like_kind session_client_like_kind_t
void session_capture_set_audio_context(session_capture_ctx_t *ctx, void *audio_ctx)
Set the audio context for playback control.
📹 Unified media capture abstraction for session-based video sources
Centralized render loop abstraction for all display modes.
bool audio_should_enable_microphone(audio_capture_source_t source, bool has_media_audio)
Determine if microphone should be enabled based on audio source setting and media state.
asciichat_error_t audio_init(audio_context_t *ctx)
Initialize audio context and PortAudio.
asciichat_error_t audio_start_duplex(audio_context_t *ctx)
Start full-duplex audio (simultaneous capture and playback)
void audio_destroy(audio_context_t *ctx)
Destroy audio context and clean up resources.
asciichat_error_t audio_stop_duplex(audio_context_t *ctx)
Stop full-duplex audio.
void audio_stop_thread()
Stop audio capture thread.
unsigned int uint32_t
Definition common.h:58
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
void debug_sync_cleanup_thread(void)
Stop and clean up background debug thread.
Definition sync.c:674
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_DISPLAY
@ ERROR_MEDIA_INIT
Definition error_codes.h:70
@ ERROR_USAGE
Definition error_codes.h:53
@ ERROR_INVALID_PARAM
@ ERROR_WEBCAM
Definition error_codes.h:64
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
void log_set_force_stderr(bool enabled)
Force all terminal log output to stderr.
Definition log/log.c:721
#define log_fatal(...)
Log a FATAL message.
Definition log/log.h:599
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
void log_set_terminal_output(bool enabled)
Control stderr output to terminal.
Definition log/log.c:700
bool media_source_has_audio(media_source_t *source)
Check if media source has audio stream.
Definition source.c:737
void media_source_destroy(media_source_t *source)
Destroy media source and free resources.
Definition source.c:448
double media_source_get_video_fps(media_source_t *source)
Get video frame rate in frames per second.
Definition source.c:978
void media_source_set_audio_context(media_source_t *source, void *audio_ctx)
Set audio context for playback buffer management.
Definition source.c:1041
media_source_t * media_source_create(media_source_type_t type, const char *path)
Create a new media source.
Definition source.c:187
@ MEDIA_SOURCE_STDIN
Piped or redirected input.
Definition source.h:84
@ MEDIA_SOURCE_WEBCAM
Hardware webcam device.
Definition source.h:82
@ MEDIA_SOURCE_FILE
Media file (video/audio)
Definition source.h:83
@ MEDIA_SOURCE_TEST
Test pattern generator.
Definition source.h:85
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition util/time.c:150
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
#define OPT_HEIGHT_DEFAULT
Default terminal height in characters.
#define OPT_WIDTH_DEFAULT
Default terminal width in characters.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all bytes to a file descriptor, handling partial writes.
Definition system.c:224
asciichat_error_t terminal_clear_screen(void)
Clear the terminal screen.
void platform_disable_keepawake(void)
Disable system sleep prevention (allow OS to sleep)
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
asciichat_error_t platform_enable_keepawake(void)
Enable system sleep prevention (keepawake mode)
void session_display_destroy(session_display_ctx_t *ctx)
Destroy session display context and free resources.
void session_display_set_render_fps(session_display_ctx_t *ctx, uint32_t fps)
Set the render FPS for file output encoding.
session_display_ctx_t * session_display_create(const session_display_config_t *config)
Create a new session display context.
void session_display_set_stdin_reader(session_display_ctx_t *ctx, void *reader)
Set stdin frame reader for ASCII-to-video rendering.
bool splash_is_running(void)
Check if splash screen animation is currently running.
Definition splash.c:725
int splash_intro_done(void)
Signal that intro splash should end (first frame ready to render)
Definition splash.c:693
void update_banner_wait_for_check(void)
Wait for the background update check thread to finish.
bool update_banner_show_prompt(session_display_ctx_t *ctx)
Show the update prompt screen and wait for user input.
void splash_restore_stderr(void)
Restore stderr after splash animation completes.
Definition splash.c:716
int splash_intro_start(session_display_ctx_t *ctx)
Start animated intro screen with rainbow ASCII art (non-blocking)
Definition splash.c:639
session_capture_ctx_t * session_mirror_capture_create(const session_capture_config_t *config)
Create a new session capture context.
session_display_ctx_t * session_display_get_global_context(void)
Get the global display context (used for signal handlers and special cleanup)
void session_display_reset_first_frame(session_display_ctx_t *ctx)
Reset the first frame flag to allow splash screen to show again on next render.
void session_display_set_global_context(session_display_ctx_t *ctx)
Set the global display context (for signal handlers like Ctrl+C)
bool update_banner_has_update(void)
Check if an update is available (thread-safe)
void splash_wait_for_animation(void)
Wait for splash animation thread to fully exit.
Definition splash.c:731
session_capture_ctx_t * session_network_capture_create(uint32_t target_fps)
Create network mode capture context without media source.
void * session_capture_get_media_source(session_capture_ctx_t *ctx)
Get the underlying media source from capture context.
void session_capture_destroy(session_capture_ctx_t *ctx)
Destroy session capture context and free resources.
void update_banner_print_instructions(void)
Print upgrade instructions to stdout and prepare for clean exit.
terminal_fd_reader_t * session_client_like_get_stdin_reader(void)
Definition client_like.c:93
asciichat_error_t webcam_list_devices(webcam_device_info_t **out_devices, unsigned int *out_count)
Enumerate available webcam devices.
void webcam_free_device_list(webcam_device_info_t *devices)
Free device list returned by webcam_list_devices()
🔊 Audio Capture and Playback Interface for ascii-chat
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
🆘 Interactive keyboard help overlay for session keyboard shortcuts
📝 Logging API with multiple log levels and terminal output control
bool should_exit(void)
Definition misc.c:131
bool terminal_is_stdin_tty(void)
Check if stdin is connected to a TTY.
bool terminal_is_stdout_tty(void)
Check if stdout is connected to a TTY.
bool terminal_should_force_stderr(void)
Determine if logs should be forced to stderr.
void terminal_fd_reader_destroy(terminal_fd_reader_t *reader)
Destroy terminal FD reader.
asciichat_error_t terminal_fd_reader_create(int fd, int frame_height, terminal_fd_reader_t **out)
Create a terminal FD reader from a file descriptor.
Read ANSI ASCII frames from file descriptor, chunked by height.
🎬 Unified media source abstraction for webcam, files, and stdin
Intro and status screen display for ascii-chat with animated rainbow effects.
asciichat_error_t session_display_init_render_file(session_display_ctx_t *ctx, uint32_t fps)
Perform complete terminal reset.
🖥️ Unified terminal display abstraction for session-based rendering
#define bool
Definition stdbool.h:61
Audio context for full-duplex capture and playback.
bool monitor_local_media
Play local file audio in solo sessions.
void * capture_media_source
Borrowed local media audio; configure before starting duplex.
bool playback_only
True for playback-only mode (mirror), skip microphone capture.
Media source for video and audio capture.
Definition source.c:34
Configuration for session capture context.
media_source_type_t type
Media source type (WEBCAM, FILE, STDIN, TEST)
const char * path
Device index (for webcam) or file path (for file sources)
uint32_t target_fps
Target frame rate in FPS (e.g., 60 for display, 144 for network)
double initial_seek_timestamp
Seek to this timestamp after opening media (0.0 = no seek)
session_capture_should_exit_fn should_exit_callback
Optional: callback to check if initialization should be cancelled (e.g., shutdown signal)
void * callback_data
Opaque data passed to should_exit_callback.
bool resize_for_network
Resize frames to network-optimal dimensions (MAX_FRAME_WIDTH x MAX_FRAME_HEIGHT)
bool loop
Enable loop playback for file sources.
bool enable_audio
Enable audio capture from media source.
Internal session capture context structure.
session_client_like_kind_t kind
bool(* should_reconnect_callback)(asciichat_error_t last_error, int attempt_number, void *user_data)
unsigned int reconnect_delay_ms
session_client_like_run_fn run_fn
bool(* custom_should_exit)(void *user_data)
Configuration for session display context.
void * callback_data
Opaque data passed to should_exit_callback.
palette_type_t palette_type
Palette type for ASCII rendering.
bool defer_render_file
Defer render-file encoder creation until media timing is known.
const char * custom_palette
Custom palette characters (required if palette_type == PALETTE_CUSTOM)
session_display_should_exit_fn should_exit_callback
Optional: callback to check if initialization should be cancelled (e.g., shutdown signal)
bool snapshot_mode
Enable snapshot mode (single frame capture)
terminal_color_mode_t color_mode
Color mode override (TERM_COLOR_AUTO for auto-detection)
Internal session display context structure.
Webcam device information structure.
Definition webcam.h:86
🔒 Synchronization primitive debugging (mutexes, rwlocks, condition variables)
🖥️ Cross-platform terminal interface for ascii-chat
@ TERM_COLOR_AUTO
Auto-detect color support from terminal capabilities.
Definition terminal.h:580
Update available prompt screen shown after splash.
🌐 Production-Grade URL Validation and Parsing