ascii-chat 0.11.33
Video chat in your terminal
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 "session/capture.h"
38#include "session/display.h"
39#include "session/host.h"
40#include "session/render.h"
42#include "session/client_like.h"
43#include "session/participant.h"
50
51#include <stdio.h>
52
53#include <ascii-chat/log/log.h>
63
64/* ============================================================================
65 * Global Discovery Session
66 * ============================================================================ */
67
76
77static void *discovery_audio_send_thread(void *user_data) {
78 discovery_video_receiver_t *ctx = user_data;
79 float samples[960];
81 while (atomic_load_bool(&ctx->running) && !should_exit()) {
84 continue;
85 }
86 if (audio_ring_buffer_read(ctx->audio->capture_buffer, samples, 960) != 960)
87 continue;
88 int size = client_audio_pipeline_capture(ctx->audio_pipeline, samples, 960, encoded, sizeof(encoded));
89 if (size <= 0)
90 continue;
91 uint16_t frame_size = (uint16_t)size;
92 asciichat_error_t result = acip_send_audio_opus_batch(ctx->transport, encoded, size, &frame_size, 1, 48000, 20);
93 if (result != ASCIICHAT_OK) {
94 log_warn("Discovery audio send failed: %d", result);
95 atomic_store_bool(&ctx->running, false);
96 break;
97 }
98 }
99 return NULL;
100}
101
102static asciichat_error_t discovery_audio_play_packet(discovery_video_receiver_t *ctx, packet_type_t type,
103 const void *payload, size_t length) {
104 if (!ctx->audio)
105 return ASCIICHAT_OK;
106 if (length < 16)
107 return SET_ERRNO(ERROR_INVALID_PARAM, "Truncated discovery audio packet");
108 float samples[5760];
109 if (type == PACKET_TYPE_AUDIO_BATCH) {
111 memcpy(&header, payload, sizeof(header));
112 if (header.sample_rate != AUDIO_SAMPLE_RATE || header.channels != 1 ||
113 header.total_samples != (length - sizeof(header)) / sizeof(float) ||
114 (length - sizeof(header)) % sizeof(float) != 0)
115 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid discovery PCM audio batch");
116 size_t offset = 0;
117 while (offset < header.total_samples) {
118 size_t count = header.total_samples - offset;
119 if (count > 5760)
120 count = 5760;
121 memcpy(samples, (const uint8_t *)payload + sizeof(header) + offset * sizeof(float), count * sizeof(float));
122 asciichat_error_t result = audio_write_samples(ctx->audio, samples, (int)count);
123 if (result != ASCIICHAT_OK)
124 return result;
125 offset += count;
126 }
127 } else if (type == PACKET_TYPE_AUDIO_OPUS_BATCH) {
128 const uint8_t *bytes = payload;
129 uint32_t rate_net, duration_net;
130 memcpy(&rate_net, bytes, 4);
131 memcpy(&duration_net, bytes + 4, 4);
132 if (NET_TO_HOST_U32(rate_net) != AUDIO_SAMPLE_RATE || NET_TO_HOST_U32(duration_net) < 1 ||
133 NET_TO_HOST_U32(duration_net) > 120)
134 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid discovery Opus audio format");
135 uint32_t count_net;
136 memcpy(&count_net, bytes + 8, 4);
137 size_t count = NET_TO_HOST_U32(count_net);
138 if (count > (length - 16) / 2)
139 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid discovery Opus batch");
140 size_t offset = 16 + count * 2;
141 for (size_t i = 0; i < count; i++) {
142 uint16_t size_net;
143 memcpy(&size_net, bytes + 16 + i * 2, 2);
144 size_t size = NET_TO_HOST_U16(size_net);
145 if (!size || size > length - offset)
146 return SET_ERRNO(ERROR_INVALID_PARAM, "Truncated discovery Opus frame");
147 int decoded = client_audio_pipeline_playback(ctx->audio_pipeline, bytes + offset, (int)size, samples, 5760);
148 if (decoded < 0)
149 return SET_ERRNO(ERROR_AUDIO, "Failed to decode discovery Opus frame");
150 if (decoded > 0) {
151 asciichat_error_t result = audio_write_samples(ctx->audio, samples, decoded);
152 if (result != ASCIICHAT_OK)
153 return result;
154 }
155 offset += size;
156 }
157 if (offset != length)
158 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid discovery Opus batch length");
159 }
160 return ASCIICHAT_OK;
161}
162
163static void *discovery_video_receive_thread(void *user_data) {
164 discovery_video_receiver_t *ctx = user_data;
165 uint64_t report_start_ns = time_get_ns();
166 uint64_t report_received_frames = 0;
167 uint64_t report_rendered_frames = 0;
168 uint64_t report_render_time_ns = 0;
169 while (atomic_load_bool(&ctx->running) && !should_exit() && acip_transport_is_connected(ctx->transport)) {
171 // Keep the newest display frame and bound receive work so capture cannot starve.
172 char *latest_text = NULL;
173 for (size_t received = 0; received < 128 && acip_transport_has_pending_data(ctx->transport); received++) {
174 packet_type_t type;
175 void *payload = NULL, *allocated = NULL;
176 size_t length = 0;
177 result = packet_receive_via_transport(ctx->transport, &type, &payload, &length, &allocated);
178 if (result == ASCIICHAT_OK && type == PACKET_TYPE_ASCII_FRAME && length >= sizeof(ascii_frame_packet_t)) {
179 report_received_frames++;
181 memcpy(&header, payload, sizeof(header));
182 uint32_t original_size = NET_TO_HOST_U32(header.original_size);
183 uint32_t compressed_size = NET_TO_HOST_U32(header.compressed_size);
184 bool is_compressed = (NET_TO_HOST_U32(header.flags) & FRAME_FLAG_IS_COMPRESSED) != 0;
185 size_t encoded_size = is_compressed ? compressed_size : original_size;
186 size_t encoded_length = length - sizeof(header);
187 if (encoded_size == encoded_length && (!is_compressed || compressed_size > 0)) {
188 char *decoded = packet_decode_frame_data_malloc((const char *)payload + sizeof(header), encoded_length,
189 is_compressed, original_size, compressed_size);
190 if (decoded) {
191 SAFE_FREE(latest_text);
192 latest_text = decoded;
193 } else {
194 log_warn_every(US_PER_SEC_INT, "Could not decode server ASCII frame (compressed=%d original=%u encoded=%u)",
195 is_compressed, original_size, compressed_size);
196 CLEAR_ERRNO();
197 }
198 } else {
200 "Invalid server ASCII frame lengths (compressed=%d original=%u encoded=%u payload=%zu)",
201 is_compressed, original_size, encoded_size, encoded_length);
202 }
203 } else if (result == ASCIICHAT_OK && type == PACKET_TYPE_PING) {
204 result = packet_send_via_transport(ctx->transport, PACKET_TYPE_PONG, payload, length, 0);
205 } else if (result == ASCIICHAT_OK && (type == PACKET_TYPE_AUDIO_BATCH || type == PACKET_TYPE_AUDIO_OPUS_BATCH)) {
206 result = discovery_audio_play_packet(ctx, type, payload, length);
207 }
208 if (allocated)
209 buffer_pool_free(NULL, allocated, 0);
210 if (result != ASCIICHAT_OK) {
211 SAFE_FREE(latest_text);
212 ctx->result = result;
213 atomic_store_bool(&ctx->running, false);
214 return NULL;
215 }
216 }
217 if (latest_text) {
218 uint64_t render_start_ns = time_get_ns();
219 session_display_render_frame(ctx->display, latest_text);
220 report_render_time_ns += time_get_ns() - render_start_ns;
221 report_rendered_frames++;
222 SAFE_FREE(latest_text);
223 latest_text = NULL;
224 }
225 uint64_t now_ns = time_get_ns();
226 uint64_t report_elapsed_ns = now_ns - report_start_ns;
227 if (report_elapsed_ns >= NS_PER_SEC_INT) {
228 log_info("Discovery ASCII receive/render rate: received=%.1f fps rendered=%.1f fps avg_render=%.2fms",
229 (double)report_received_frames * NS_PER_SEC_INT / (double)report_elapsed_ns,
230 (double)report_rendered_frames * NS_PER_SEC_INT / (double)report_elapsed_ns,
231 report_rendered_frames ? (double)report_render_time_ns / (double)report_rendered_frames / 1e6 : 0.0);
232 report_start_ns = now_ns;
233 report_received_frames = 0;
234 report_rendered_frames = 0;
235 report_render_time_ns = 0;
236 }
238 }
239 return NULL;
240}
241
243static discovery_session_t *g_discovery = NULL;
244
245/* ============================================================================
246 * State Change Callback
247 * ============================================================================ */
248
255static void on_discovery_state_change(discovery_state_t new_state, void *user_data) {
256 (void)user_data; // Unused
257
258 const char *state_names[] = {"INIT", "CONNECTING_ACDS", "CREATING_SESSION", "JOINING_SESSION",
259 "WAITING_PEER", "NEGOTIATING", "STARTING_HOST", "CONNECTING_HOST",
260 "ACTIVE", "MIGRATING", "FAILED", "ENDED"};
261
262 if (new_state >= 0 && new_state < (int)(sizeof(state_names) / sizeof(state_names[0]))) {
263 log_info("Discovery state: %s", state_names[new_state]);
264 }
265}
266
273static void on_session_ready(const char *session_string, void *user_data) {
274 (void)user_data; // Unused
275
276 if (session_string && session_string[0]) {
277 log_info("Session ready! Share this with your peer: %s", session_string);
278 }
279}
280
288static void on_discovery_error(asciichat_error_t error, const char *message, void *user_data) {
289 (void)user_data; // Unused
290
291 log_error("Discovery error (%d): %s", error, message ? message : "Unknown");
292 signal_exit();
293}
294
302static bool discovery_capture_should_exit_adapter(void *user_data) {
303 (void)user_data; // Unused parameter
304 return should_exit();
305}
306
317static void discovery_keyboard_handler(session_capture_ctx_t *capture, int key, void *user_data) {
318 (void)user_data; // Unused parameter
319 // Discovery mode doesn't have a display context yet, pass NULL for display
320 // Help screen will be silently ignored (no-op)
322}
323
324/* ============================================================================
325 * Discovery Run Callback (for session_client_like)
326 * ============================================================================ */
327
339static asciichat_error_t discovery_run(session_capture_ctx_t *capture, session_display_ctx_t *display,
340 void *user_data) {
341 (void)user_data; // Unused
342
344 bool snapshot_mode = GET_OPTION(snapshot_mode);
345
346 // Wait for session to become active (host negotiation complete)
347 // This processes ACDS events until we have a determined role
348 // Snapshot capture requires negotiation to finish before its exit condition is honored.
349 // The snapshot delay will be honored once we transition to active state and start capturing
350 while (true) {
351 if (should_exit() && !snapshot_mode) {
352 return ASCIICHAT_OK;
353 }
354 result = discovery_session_process(g_discovery, 50 * NS_PER_MS_INT);
355 if (result != ASCIICHAT_OK && result != ERROR_NETWORK_TIMEOUT) {
356 log_error("Discovery session process failed: %d", result);
357 return result;
358 }
359
360 if (discovery_session_is_active(g_discovery)) {
361 break; // Session is active, role is determined
362 }
364 }
365
366 // In snapshot mode, proceed to role handling even if should_exit() is true
367 // We'll check for exit conditions during the actual frame rendering phase
368 if (should_exit() && !snapshot_mode) {
369 log_info("Shutdown requested during discovery negotiation");
370 return ASCIICHAT_OK;
371 }
372
373 // Session is active - run based on our role
374 bool we_are_host = discovery_session_is_host(g_discovery);
375
376 if (we_are_host) {
377 // HOST ROLE: Capture own media, manage participants, mix and broadcast
378 log_info("Hosting session - capturing and broadcasting");
379
380 // Keep terminal logging enabled until a peer connects and we have video to render.
381 // This keeps the splash/log UI visible while waiting for peers instead of a blank screen.
382
383 // Get the host context already created and started during discovery negotiation
384 session_host_t *host = discovery_session_get_host(g_discovery);
385 if (!host) {
386 log_fatal("Failed to get session host from discovery");
387 return ERROR_INVALID_STATE;
388 }
389
390 // Set display context so host renders frames locally
391 asciichat_error_t display_result = session_host_set_display(host, display);
392 if (display_result != ASCIICHAT_OK) {
393 log_error("Failed to set display context on host: %d", display_result);
394 // Non-fatal - host can still render frames for broadcasting, just not locally
395 } else {
396 log_info("Display context set on host for local frame rendering");
397 }
398
399 // The passed capture context is set up for network mode (participant receiving frames).
400 // For host role, we need local media capture. Reuse the existing capture context
401 // which should have test pattern, webcam, or file configured.
402 // Note: session_client_like should have initialized this with the media source.
403
404 // Add memory participant for host's own media
405 uint32_t host_participant_id = session_host_add_memory_participant(host);
406 if (host_participant_id == 0) {
407 log_error("Failed to add memory participant for host");
408 // Host context is owned by discovery session - don't destroy
409 return ERROR_INVALID_STATE;
410 }
411
412 log_info("Host participating with ID %u", host_participant_id);
413
414 // Start the render thread (handles video mixing and ASCII rendering for display)
415 result = session_host_start_render(host);
416 if (result != ASCIICHAT_OK) {
417 log_error("Failed to start render thread: %d", result);
418 // Host context is owned by discovery session - don't destroy
419 return result;
420 }
421
422 // Main loop: capture own media and keep discovery responsive
423 while (discovery_session_is_active(g_discovery)) {
424 // Exit conditions:
425 // 1. Non-snapshot mode: check should_exit() (Ctrl+C, SIGTERM, any exit signal)
426 // 2. Snapshot mode: exit when should_exit() is true AND we've rendered at least one frame
427 // (to allow the full snapshot_delay to elapse while ignoring the snapshot timeout in should_exit)
428 // 3. Discovery session ended: always exit
429
430 if (should_exit()) {
431 // In snapshot mode, if we haven't rendered any frames yet, don't exit yet
432 // Wait until at least one frame has been displayed
433 if (!snapshot_mode || g_snapshot_first_frame_rendered) {
434 log_debug("Exit condition met (snapshot_mode=%d, first_frame_rendered=%d)", snapshot_mode,
436 break;
437 } else {
439 "Waiting for first frame (snapshot_mode=true, first_frame_rendered=false)");
440 }
441 }
442
443 // Capture frame from local media (webcam, test pattern, file, etc.)
444 // The capture context is set up during session_client_like_run() and handles all media types
446 if (frame) {
447 log_debug_every(5 * NS_PER_SEC_INT, "Captured frame, injecting into host");
448 // Inject host's frame into mixer for broadcasting
449 session_host_inject_frame(host, host_participant_id, frame);
450 } else {
451 log_debug_every(5 * NS_PER_SEC_INT, "No frame captured");
452 }
453
454 // Keep discovery session responsive (NAT negotiations, migrations)
455 result = discovery_session_process(g_discovery, 10 * NS_PER_MS_INT);
456 if (result != ASCIICHAT_OK && result != ERROR_NETWORK_TIMEOUT) {
457 log_error("Discovery session process failed: %d", result);
458 break;
459 }
460
461 // Frame rate limiting (60 FPS)
463 }
464
465 log_debug("Host role main loop exited (snapshot_mode=%d, first_frame_rendered=%d, session_active=%d)",
466 snapshot_mode, g_snapshot_first_frame_rendered ? 1 : 0, discovery_session_is_active(g_discovery) ? 1 : 0);
467
468 // Stop render thread before returning (display will be destroyed by caller)
470
471 if (should_exit()) {
472 return ASCIICHAT_OK;
473 }
474
475 // If we reach here, discovery session is no longer active (role change)
476 if (!discovery_session_is_active(g_discovery)) {
477 log_info("Session ended or role changed");
478 return ASCIICHAT_OK;
479 }
480 } else {
482 acip_transport_t *transport = session_participant_get_transport(participant);
483 if (transport && acip_transport_get_type(transport) == ACIP_TRANSPORT_WEBRTC) {
484 bool enable_audio = GET_OPTION(audio_enabled);
485 log_info("Discovery WebRTC audio: %s", enable_audio ? "enabled" : "disabled");
486 result =
487 acip_send_client_join(transport, CLIENT_CAP_VIDEO | CLIENT_CAP_COLOR | (enable_audio ? CLIENT_CAP_AUDIO : 0));
488 if (result != ASCIICHAT_OK)
489 return result;
496 packet.width = HOST_TO_NET_U16(GET_OPTION(width) > 0 ? GET_OPTION(width) : 80);
497 packet.height = HOST_TO_NET_U16(GET_OPTION(height) > 0 ? GET_OPTION(height) : 24);
498 packet.palette_type = HOST_TO_NET_U32(GET_OPTION(palette_type));
499 packet.utf8_support = caps.utf8_support ? 1 : 0;
500 int desired_fps = GET_OPTION(fps) > 0 ? GET_OPTION(fps) : caps.desired_fps;
501 if (desired_fps <= 0) {
502 desired_fps = DEFAULT_MAX_FPS;
503 }
504 packet.desired_fps = (uint8_t)(desired_fps > 144 ? 144 : desired_fps);
505 packet.wants_padding = caps.wants_padding ? 1 : 0;
507 SAFE_STRNCPY(packet.term_type, caps.term_type, sizeof(packet.term_type));
508 SAFE_STRNCPY(packet.colorterm, caps.colorterm, sizeof(packet.colorterm));
509 result = acip_send_capabilities(transport, &packet, sizeof(packet));
510 if (result != ASCIICHAT_OK)
511 return result;
512 result = acip_send_stream_start(transport, STREAM_TYPE_VIDEO | (enable_audio ? STREAM_TYPE_AUDIO : 0));
513 if (result != ASCIICHAT_OK)
514 return result;
515 discovery_video_receiver_t receiver = {
516 .transport = transport, .display = display, .running = {0}, .result = ASCIICHAT_OK};
517 audio_context_t audio = {0};
518 if (enable_audio) {
519 result = audio_init(&audio);
520 if (result != ASCIICHAT_OK)
521 return result;
523 bool has_media_audio = media_source_has_audio(local_source);
524 audio.capture_media_source = local_source;
525 audio.playback_only = !audio_should_enable_microphone(GET_OPTION(audio_capture_source), has_media_audio);
526 log_info("Discovery audio input: media=%d, microphone=%d", has_media_audio,
527 audio_should_enable_microphone(GET_OPTION(audio_capture_source), has_media_audio));
529 config.flags.jitter_buffer = false;
531 if (!receiver.audio_pipeline) {
532 audio_destroy(&audio);
533 return SET_ERRNO(ERROR_AUDIO, "Failed to create discovery audio pipeline");
534 }
535 audio_set_pipeline(&audio, receiver.audio_pipeline);
536 receiver.audio = &audio;
537 result = audio_start_duplex(&audio);
538 if (result != ASCIICHAT_OK) {
539 audio_destroy(&audio);
541 return result;
542 }
543 }
544 asciichat_thread_t receiver_thread;
545 asciichat_thread_t audio_thread;
546 bool audio_thread_started = false;
547 atomic_store_bool(&receiver.running, true);
548 result = asciichat_thread_create(&receiver_thread, "discovery_video_receive", discovery_video_receive_thread,
549 &receiver);
550 if (result != ASCIICHAT_OK) {
551 if (enable_audio) {
552 audio_destroy(&audio);
554 }
555 return result;
556 }
557 if (enable_audio) {
558 result = asciichat_thread_create(&audio_thread, "discovery_audio_send", discovery_audio_send_thread, &receiver);
559 audio_thread_started = result == ASCIICHAT_OK;
560 if (!audio_thread_started)
561 atomic_store_bool(&receiver.running, false);
562 }
563 while (!should_exit() && discovery_session_is_active(g_discovery) && acip_transport_is_connected(transport) &&
564 atomic_load_bool(&receiver.running)) {
565 uint64_t iteration_start = time_get_ns();
566 if (snapshot_mode && g_snapshot_first_frame_rendered_ns > 0 &&
567 time_ns_to_s(time_elapsed_ns(g_snapshot_first_frame_rendered_ns, iteration_start)) >=
568 GET_OPTION(snapshot_delay)) {
569 signal_exit();
570 break;
571 }
572 uint64_t capture_start = time_get_ns();
574 uint64_t capture_end = time_get_ns();
575 uint64_t processing_start = capture_end;
576 uint64_t processing_end = processing_start;
577 uint64_t send_start = processing_start;
578 uint64_t send_end = send_start;
579 if (frame) {
581 if (processed) {
582 processing_end = time_get_ns();
583 send_start = processing_end;
584 result = acip_send_image_frame(transport, processed->pixels, processed->w, processed->h, PIXEL_FORMAT_RGB);
585 send_end = time_get_ns();
586 image_destroy(processed);
587 if (result != ASCIICHAT_OK)
588 break;
589 }
590 }
591 if (processing_end == processing_start)
592 processing_end = time_get_ns();
593 uint64_t signaling_start = send_end;
594 result = discovery_session_process(g_discovery, 10 * NS_PER_MS_INT);
595 uint64_t signaling_end = time_get_ns();
596 if (result != ASCIICHAT_OK && result != ERROR_NETWORK_TIMEOUT)
597 break;
598 uint64_t event_start = time_get_ns();
599 APP_CALLBACK_VOID(platform_pump_events);
600 uint64_t event_end = time_get_ns();
601 uint64_t interval = NS_PER_SEC_INT / (GET_OPTION(fps) > 0 ? GET_OPTION(fps) : 30);
602 uint64_t elapsed = time_get_ns() - iteration_start;
603 if (elapsed > 50 * NS_PER_MS_INT) {
605 "Discovery media loop took %.1fms (capture=%.1fms process=%.1fms send=%.1fms signaling=%.1fms "
606 "events=%.1fms)",
607 (double)elapsed / 1e6, (double)(capture_end - capture_start) / 1e6,
608 (double)(processing_end - processing_start) / 1e6, (double)(send_end - send_start) / 1e6,
609 (double)(signaling_end - signaling_start) / 1e6, (double)(event_end - event_start) / 1e6);
610 }
611 if (elapsed < interval)
612 platform_sleep_ns(interval - elapsed);
613 }
614 atomic_store_bool(&receiver.running, false);
615 asciichat_thread_join(&receiver_thread, NULL);
616 if (audio_thread_started)
617 asciichat_thread_join(&audio_thread, NULL);
618 if (enable_audio) {
619 audio_destroy(&audio);
621 }
622 if (receiver.result != ASCIICHAT_OK)
623 return receiver.result;
624 if (result != ASCIICHAT_OK && result != ERROR_NETWORK_TIMEOUT)
625 return result;
626 return ASCIICHAT_OK;
627 }
628 // PARTICIPANT ROLE: Just wait for host connection
629 // Don't use capture/display for participant discovery mode
630 (void)capture; // Not used
631
632 // For participants, destroy the display context to reset terminal state
633 // and prevent the discovery framework from rendering anything
634 if (display) {
635 log_info("Participant mode - cleaning up display context");
637 // Also clear the global display context to prevent signal handlers from using stale pointer
639 }
640
641 // Session is active and we're a participant. In discovery mode, participants
642 // don't render anything - they just wait for the host. Return early to skip
643 // any display rendering that would happen after discovery_run returns.
644 return ASCIICHAT_OK;
645 }
646
647 return result;
648}
649
650/* ============================================================================
651 * Main Discovery Mode Loop
652 * ============================================================================ */
653
661int discovery_main(void) {
662 log_debug("discovery_main() starting");
663
664 // Discovery-specific setup: initialize ACDS connection and session negotiation
665 // Note: Shared setup (keepawake, splash, terminal, capture, display, audio)
666 // is handled by session_client_like_run()
667
668 // Get session string from options (command-line argument)
669 const char *session_string = GET_OPTION(session_string);
670 bool is_initiator = (session_string == NULL || session_string[0] == '\0');
671
672 int port_int = GET_OPTION(port);
673
674 log_debug("Discovery: is_initiator=%d, port=%d", is_initiator, port_int);
675
676 // Validate mutually exclusive options: discovery_service_url vs discovery_port
677 const char *service_url = GET_OPTION(discovery_service_url);
678 if (service_url && service_url[0] != '\0') {
679 int discovery_port = GET_OPTION(discovery_port);
680 if (discovery_port != ACIP_DISCOVERY_DEFAULT_PORT) {
681 log_fatal("--discovery-service-url and --discovery-service-port are mutually exclusive");
682 return ERROR_USAGE;
683 }
684 }
685
686 // Create discovery session configuration
687 discovery_config_t discovery_config = {
688 .acds_address = GET_OPTION(discovery_server),
689 .acds_port = (uint16_t)GET_OPTION(discovery_port),
690 .acds_url = service_url,
691 .session_string = is_initiator ? NULL : session_string,
692 .password = GET_OPTION(password),
693 .local_port = (uint16_t)port_int,
694 .on_state_change = on_discovery_state_change,
695 .on_session_ready = on_session_ready,
696 .on_error = on_discovery_error,
697 .callback_user_data = NULL,
698 .should_exit_callback = discovery_capture_should_exit_adapter,
699 .exit_callback_data = NULL,
700 };
701
702 // Create discovery session (stored in global for discovery_run() to access)
703 g_discovery = discovery_session_create(&discovery_config);
704 if (!g_discovery) {
705 log_fatal("Failed to create discovery session");
706 return ERROR_MEMORY;
707 }
708
709 // Start discovery session (connects to ACDS, creates/joins, initiates NAT negotiation)
710 log_debug("Discovery: starting discovery session");
711 asciichat_error_t result = discovery_session_start(g_discovery);
712 if (result != ASCIICHAT_OK) {
713 log_fatal("Failed to start discovery session: %d", result);
714 discovery_session_destroy(g_discovery);
715 g_discovery = NULL;
716 return result;
717 }
718
719 // No network interrupt callback needed - discovery session handles its own shutdown
721
722 /* ========================================================================
723 * Configure and Run Shared Client-Like Session Framework
724 * ========================================================================
725 * session_client_like_run() handles all shared initialization:
726 * - Terminal output management
727 * - Keepawake system
728 * - Splash screen lifecycle
729 * - Media source selection
730 * - FPS probing
731 * - Audio initialization
732 * - Display context creation
733 * - Proper cleanup ordering
734 *
735 * Discovery mode provides:
736 * - discovery_run() callback: NAT negotiation, role determination, media handling
737 * - discovery_keyboard_handler: interactive controls for participant role
738 */
739
741 .run_fn = discovery_run,
742 .run_user_data = NULL,
744 .discovery = (void *)g_discovery, // Opaque pointer to discovery session
745 .custom_should_exit = NULL,
746 .exit_user_data = NULL,
747 .keyboard_handler = discovery_keyboard_handler,
748 .max_reconnect_attempts = 0, // Discovery doesn't retry - role is determined once
749 .should_reconnect_callback = NULL,
750 .reconnect_user_data = NULL,
751 .reconnect_delay_ms = 0,
752 .print_newline_on_tty_exit = false, // Server/participant manages cursor
753 };
754
755 log_debug("Discovery: calling session_client_like_run()");
756 asciichat_error_t session_result = session_client_like_run(&config);
757 log_debug("Discovery: session_client_like_run() returned %d", session_result);
758
759 // Cleanup discovery session
760 log_debug("Discovery: cleaning up");
761
762 if (g_discovery) {
763 discovery_session_stop(g_discovery);
764 discovery_session_destroy(g_discovery);
765 g_discovery = NULL;
766 }
767
768 return (session_result == ASCIICHAT_OK) ? 0 : (int)session_result;
769}
Platform abstraction layer umbrella header providing unified cross-platform API.
ascii-chat Discovery Service (ACDS) Protocol Message Formats
Application-level callbacks for library code.
#define APP_CALLBACK_VOID(callback_name)
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
🗃️ Lock-Free Unified Memory Buffer Pool with Lazy Allocation
asciichat_error_t session_client_like_run(const session_client_like_config_t *config)
Shared initialization and teardown for client-like modes (mirror, client, discovery)
@ SESSION_CLIENT_LIKE_KIND_DISCOVERY
Definition client_like.h:66
int client_audio_pipeline_playback(client_audio_pipeline_t *pipeline, const uint8_t *opus_in, int opus_len, float *output, int num_samples)
Decode Opus packet and process for playback.
client_audio_pipeline_t * client_audio_pipeline_create(const client_audio_pipeline_config_t *config)
Create and initialize a client audio pipeline.
client_audio_pipeline_config_t client_audio_pipeline_default_config(void)
Get default configuration.
int client_audio_pipeline_capture(client_audio_pipeline_t *pipeline, const float *input, int num_samples, uint8_t *opus_out, int max_opus_len)
Process captured audio and encode to Opus.
void client_audio_pipeline_destroy(client_audio_pipeline_t *pipeline)
Destroy a client audio pipeline.
Unified client-side audio processing pipeline.
#define CLIENT_AUDIO_PIPELINE_MAX_OPUS_PACKET
📹 Unified media capture abstraction for session-based video sources
uint64_t g_snapshot_first_frame_rendered_ns
Centralized render loop abstraction for all display modes.
int discovery_main(void)
Run discovery mode main loop.
🔄 Network byte order conversion helpers
#define HOST_TO_NET_U16(val)
Definition endian.h:96
#define HOST_TO_NET_U32(val)
Definition endian.h:66
#define NET_TO_HOST_U16(val)
Definition endian.h:111
#define NET_TO_HOST_U32(val)
Definition endian.h:81
#define ACIP_DISCOVERY_DEFAULT_PORT
Discovery server default port (use OPT_ACDS_PORT_INT_DEFAULT from options.h)
Definition acds.h:1094
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.
size_t audio_ring_buffer_read(audio_ring_buffer_t *rb, float *data, size_t samples)
Read audio samples from ring buffer.
#define AUDIO_SAMPLE_RATE
Audio sample rate (48kHz professional quality, Opus-compatible)
asciichat_error_t audio_init(audio_context_t *ctx)
Initialize audio context and PortAudio.
size_t audio_ring_buffer_available_read(audio_ring_buffer_t *rb)
Get number of samples available for reading.
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_write_samples(audio_context_t *ctx, const float *buffer, int num_samples)
Write audio samples to playback buffer.
void audio_set_pipeline(audio_context_t *ctx, void *pipeline)
Set audio pipeline for echo cancellation.
void buffer_pool_free(buffer_pool_t *pool, const void *data, size_t size)
Free a buffer back to the pool (lock-free)
unsigned short uint16_t
Definition common.h:57
unsigned int uint32_t
Definition common.h:58
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
#define CLEAR_ERRNO()
Clear the current error state.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_INVALID_STATE
@ ERROR_AUDIO
Definition error_codes.h:67
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_USAGE
Definition error_codes.h:53
@ ERROR_INVALID_PARAM
@ ERROR_NETWORK_TIMEOUT
Definition error_codes.h:80
#define DEFAULT_MAX_FPS
Default maximum frame rate (frames per second)
Definition limits.h:29
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#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
bool media_source_has_audio(media_source_t *source)
Check if media source has audio stream.
Definition source.c:737
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
#define NS_PER_SEC_INT
Definition time.h:157
#define NS_PER_MS_INT
Definition time.h:156
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 US_PER_SEC_INT
Definition time.h:161
#define STREAM_TYPE_VIDEO
Video stream.
Definition packet.h:937
#define STREAM_TYPE_AUDIO
Audio stream.
Definition packet.h:938
#define CLIENT_CAP_COLOR
Client supports color rendering.
Definition packet.h:925
#define CLIENT_CAP_AUDIO
Client can send/receive audio.
Definition packet.h:924
#define CLIENT_CAP_VIDEO
Client can send/receive video.
Definition packet.h:923
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
char * packet_decode_frame_data_malloc(const char *frame_data_ptr, size_t frame_data_len, bool is_compressed, uint32_t original_size, uint32_t compressed_size)
Decode frame data (malloc version for client handlers)
packet_type_t
Network protocol packet type enumeration.
Definition packet.h:286
@ PACKET_TYPE_AUDIO_OPUS_BATCH
Batched Opus-encoded audio frames.
Definition packet.h:374
@ PACKET_TYPE_PONG
Keepalive pong response.
Definition packet.h:385
@ PACKET_TYPE_ASCII_FRAME
Complete ASCII frame with all metadata.
Definition packet.h:361
@ PACKET_TYPE_PING
Keepalive ping packet.
Definition packet.h:383
@ PACKET_TYPE_AUDIO_BATCH
Batched audio packets for efficiency.
Definition packet.h:372
void platform_sleep_ns(uint64_t ns)
Platform-safe sleep function with nanosecond precision.
keyboard_key_t
Unified keyboard key code enumeration.
Definition keyboard.h:54
#define FRAME_FLAG_IS_COMPRESSED
Frame data is compressed.
#define PIXEL_FORMAT_RGB
Packed RGB24 pixel format used by ACIP image-frame senders.
void session_display_destroy(session_display_ctx_t *ctx)
Destroy session display context and free resources.
void session_host_stop_render(session_host_t *host)
Stop media rendering thread.
Definition host.c:1630
image_t * session_capture_process_for_transmission(session_capture_ctx_t *ctx, image_t *frame)
Process a frame for network transmission (resize if needed)
asciichat_error_t session_host_inject_frame(session_host_t *host, uint32_t participant_id, const image_t *frame)
Inject a video frame from memory participant.
Definition host.c:1303
acip_transport_t * session_participant_get_transport(session_participant_t *p)
Get the current transport for the participant.
uint32_t session_host_add_memory_participant(session_host_t *host)
Add a memory participant (host's own media)
Definition host.c:1227
asciichat_error_t session_host_set_display(session_host_t *host, struct session_display_ctx *display)
Set display context for local frame rendering.
Definition host.c:1558
void session_display_set_global_context_public(session_display_ctx_t *ctx)
Set the global display context (public setter for discovery cleanup)
void session_handle_keyboard_input(session_capture_ctx_t *capture, session_display_ctx_t *display, keyboard_key_t key)
Handle keyboard input in a session.
void session_display_render_frame(session_display_ctx_t *ctx, const char *frame_data)
Render an ASCII frame to the terminal.
image_t * session_capture_read_frame(session_capture_ctx_t *ctx)
Read the next video frame from the capture source.
void * session_capture_get_media_source(session_capture_ctx_t *ctx)
Get the underlying media source from capture context.
void session_capture_sleep_for_fps(session_capture_ctx_t *ctx)
Sleep to maintain target frame rate.
asciichat_error_t session_host_start_render(session_host_t *host)
Start media rendering thread (video mixing and audio distribution)
Definition host.c:1571
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
🏠 Server-side session hosting abstraction
🔊 Audio Capture and Playback Interface for ascii-chat
ACIP client-side protocol API.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
🎮 Cross-platform keyboard input interface for ascii-chat
🎮 Keyboard input handler for interactive session controls
asciichat_error_t acip_send_image_frame(acip_transport_t *transport, const void *pixel_data, uint32_t width, uint32_t height, uint32_t pixel_format)
Send image frame to server (client → server)
asciichat_error_t acip_send_stream_start(acip_transport_t *transport, uint8_t stream_types)
Request to start media streaming (client → server)
asciichat_error_t acip_send_capabilities(acip_transport_t *transport, const void *cap_data, size_t cap_len)
Send terminal capabilities to server (client → server)
asciichat_error_t acip_send_client_join(acip_transport_t *transport, uint8_t capabilities)
Announce client join to server (client → server)
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)
void * asciichat_thread_t
#define asciichat_thread_join(thread, timeout_ms)
📝 Logging API with multiple log levels and terminal output control
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:702
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:708
void signal_exit(void)
Definition misc.c:129
void set_interrupt_callback(void(*callback)(void))
Definition misc.c:125
bool should_exit(void)
Definition misc.c:131
Common utilities and helpers for option parsing across all modes.
👤 Client-side session participation abstraction
bool g_snapshot_first_frame_rendered
terminal_capabilities_t detect_terminal_capabilities(void)
Detect terminal capabilities.
asciichat_error_t packet_receive_via_transport(acip_transport_t *transport, packet_type_t *type, void **payload, size_t *payload_len, void **alloc_buffer)
Receive a packet via transport with header parsing and validation.
Definition send.c:122
asciichat_error_t acip_send_audio_opus_batch(acip_transport_t *transport, const void *opus_data, size_t opus_len, const uint16_t *frame_sizes, uint32_t frame_count, uint32_t sample_rate, uint32_t frame_duration)
Send batched Opus-encoded audio frames.
Definition send.c:223
asciichat_error_t packet_send_via_transport(acip_transport_t *transport, packet_type_t type, const void *payload, size_t payload_len, uint32_t client_id)
Send packet via transport with proper header (exported for generic wrappers)
Definition send.c:41
ACIP shared/bidirectional packet sending functions.
ascii-chat Server Mode Entry Point Header
int frame
Definition splash.c:99
🖥️ Unified terminal display abstraction for session-based rendering
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.
session_host_t * discovery_session_get_host(discovery_session_t *session)
Get host context (if we are host)
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.
session_participant_t * discovery_session_get_participant(discovery_session_t *session)
Get participant context (if we are participant)
discovery_state_t
Discovery session state.
Transport instance structure.
Definition transport.h:214
ASCII frame packet structure (Packet Type 2)
Definition packet.h:848
uint32_t original_size
Size of original uncompressed ASCII data in bytes.
Definition packet.h:854
uint32_t compressed_size
Size of compressed data (0 = not compressed)
Definition packet.h:856
uint32_t flags
Frame flags bitmask (HAS_COLOR, IS_COMPRESSED, etc.)
Definition packet.h:860
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
Audio batch packet structure (Packet Type 28)
Definition packet.h:904
uint32_t channels
Number of audio channels (1=mono, 2=stereo)
Definition packet.h:912
uint32_t sample_rate
Sample rate in Hz (e.g., 44100, 48000)
Definition packet.h:910
uint32_t total_samples
Total audio samples across all chunks (typically 8192)
Definition packet.h:908
Audio context for full-duplex capture and playback.
void * capture_media_source
Borrowed local media audio; configure before starting duplex.
audio_ring_buffer_t * capture_buffer
OLD: Worker → Network (will become processed_capture_rb)
bool playback_only
True for playback-only mode (mirror), skip microphone capture.
Pipeline configuration parameters.
client_audio_pipeline_flags_t flags
Client audio pipeline state.
Configuration for discovery session.
const char * acds_address
ACDS address (default: "127.0.0.1")
Discovery session context.
acip_transport_t * transport
client_audio_pipeline_t * audio_pipeline
session_display_ctx_t * display
Image structure.
int w
Image width in pixels (must be > 0)
int h
Image height in pixels (must be > 0)
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order)
Media source for video and audio capture.
Definition source.c:34
Internal session capture context structure.
Internal session display context structure.
Internal session host structure.
Definition host.c:106
Internal session participant structure.
Definition participant.c:45
Terminal capabilities packet structure (Packet Type 5)
Definition packet.h:1018
uint32_t capabilities
Terminal capabilities bitmask (TERM_CAP_* flags)
Definition packet.h:1020
uint32_t color_level
Color level enum value (terminal_color_mode_t)
Definition packet.h:1022
uint32_t color_count
Actual color count (16, 256, or 16777216)
Definition packet.h:1024
uint16_t height
Terminal height in characters.
Definition packet.h:1030
uint8_t wants_padding
Whether client wants frame padding (1=padding enabled, 0=no padding for snapshot/piped modes)
Definition packet.h:1048
uint32_t render_mode
Render mode enum value (foreground/background/half-block)
Definition packet.h:1026
uint16_t width
Terminal width in characters.
Definition packet.h:1028
uint8_t desired_fps
Client's desired frame rate (1-144 FPS)
Definition packet.h:1044
uint8_t detection_reliable
Detection reliability flag (1=reliable detection, 0=best guess)
Definition packet.h:1036
char colorterm[32]
$COLORTERM environment variable value (for debugging)
Definition packet.h:1034
uint32_t palette_type
Palette type enum value (palette_type_t)
Definition packet.h:1040
uint32_t utf8_support
UTF-8 support flag (0=no UTF-8, 1=UTF-8 supported)
Definition packet.h:1038
char term_type[32]
$TERM environment variable value (for debugging)
Definition packet.h:1032
Complete terminal capabilities structure.
Definition terminal.h:709
terminal_color_mode_t color_level
Detected color support level (terminal_color_mode_t)
Definition terminal.h:711
render_mode_t render_mode
Preferred rendering mode (render_mode_t)
Definition terminal.h:721
bool utf8_support
True if terminal supports UTF-8 encoding.
Definition terminal.h:717
bool wants_padding
Whether client wants frame padding (centering) - false for snapshot/piped modes.
Definition terminal.h:737
uint32_t capabilities
Capability flags bitmask (terminal_capability_flags_t)
Definition terminal.h:713
uint32_t color_count
Maximum number of colors (2, 16, 256, or 16777216)
Definition terminal.h:715
char term_type[64]
$TERM environment variable value (for debugging)
Definition terminal.h:723
char colorterm[64]
$COLORTERM environment variable value (for debugging)
Definition terminal.h:725
bool detection_reliable
True if detection is confident (reliable detection)
Definition terminal.h:719
🖥️ Cross-platform terminal interface for ascii-chat
⏱️ High-precision timing utilities using sokol_time.h and uthash
@ ACIP_TRANSPORT_WEBRTC
WebRTC DataChannel (P2P)
Definition transport.h:96