ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
host.c
Go to the documentation of this file.
17#include "session/host.h"
18#include "session/display.h"
19#include <ascii-chat/common.h>
27#include <ascii-chat/log/log.h>
37#include "session/audio.h"
43
44#include <string.h>
45#include <time.h>
46#include <stdio.h>
47#include <errno.h>
48
49/* ============================================================================
50 * Session Host Constants
51 * ============================================================================ */
52
54#define SESSION_HOST_DEFAULT_MAX_CLIENTS 32
55
56/* ============================================================================
57 * Session Host Internal Types
58 * ============================================================================ */
59
85
86static asciichat_error_t session_host_send_packet(session_host_client_t *client, packet_type_t type,
87 const void *payload, size_t payload_len) {
88 if (client->transport) {
89 return packet_send_via_transport(client->transport, type, payload, payload_len, 0);
90 }
91 if (client->socket != INVALID_SOCKET_VALUE) {
92 return packet_send(client->socket, type, payload, payload_len);
93 }
94 return SET_ERRNO(ERROR_INVALID_STATE, "Session host client has no active transport");
95}
96
97/* ============================================================================
98 * Session Host Context Structure
99 * ============================================================================ */
100
188
189/* ============================================================================
190 * Session Host Lifecycle Functions
191 * ============================================================================ */
192
194 if (!config) {
195 SET_ERRNO(ERROR_INVALID_PARAM, "session_host_create: NULL config");
196 return NULL;
197 }
198
199 // Allocate host
201
202 // Copy configuration
203 host->port = config->port > 0 ? config->port : OPT_PORT_INT_DEFAULT;
206
207 if (config->ipv4_address) {
208 SAFE_STRNCPY(host->ipv4_address, config->ipv4_address, sizeof(host->ipv4_address));
209 }
210
211 if (config->ipv6_address) {
212 SAFE_STRNCPY(host->ipv6_address, config->ipv6_address, sizeof(host->ipv6_address));
213 }
214
215 if (config->key_path) {
216 SAFE_STRNCPY(host->key_path, config->key_path, sizeof(host->key_path));
217 }
218
219 if (config->password) {
220 SAFE_STRNCPY(host->password, config->password, sizeof(host->password));
221 }
222
223 host->callbacks = config->callbacks;
224 host->user_data = config->user_data;
225
226 // Initialize sockets to invalid
229 host->running = false;
230
231 // Allocate client array
233
234 host->client_count = 0;
235 host->next_client_id = 1;
236
237 // Initialize mutex
238 if (mutex_init(&host->clients_mutex, "host_clients") != 0) {
239 SET_ERRNO(ERROR_THREAD, "Failed to initialize clients mutex");
240 SAFE_FREE(host->clients);
241 SAFE_FREE(host);
242 return NULL;
243 }
244
245 host->initialized = true;
246 return host;
247}
248
250 if (!host) {
251 return;
252 }
253
254 // Stop if running
255 if (host->running) {
256 session_host_stop(host);
257 }
258
259 // Clean up audio resources
260 if (host->audio_ctx) {
262 host->audio_ctx = NULL;
263 }
264 if (host->opus_decoder) {
266 host->opus_decoder = NULL;
267 }
268
269 // Close sockets
270 if (host->socket_v4 != INVALID_SOCKET_VALUE) {
271 socket_close(host->socket_v4);
273 }
274 if (host->socket_v6 != INVALID_SOCKET_VALUE) {
275 socket_close(host->socket_v6);
277 }
278
279 // Free client array and per-client resources
280 if (host->clients) {
281 for (int i = 0; i < host->max_clients; i++) {
282 if (host->clients[i].incoming_video) {
284 host->clients[i].incoming_video = NULL;
285 }
286 if (host->clients[i].incoming_audio) {
288 host->clients[i].incoming_audio = NULL;
289 }
290 }
291 SAFE_FREE(host->clients);
292 }
293
294 // Destroy mutex
296
297 // Clear sensitive data
298 memset(host->password, 0, sizeof(host->password));
299
300 host->initialized = false;
301 SAFE_FREE(host);
302}
303
304/* ============================================================================
305 * Session Host Server Control Functions
306 * ============================================================================ */
307
312static socket_t create_listen_socket(const char *address, int port) {
313 struct addrinfo hints, *result = NULL, *rp = NULL;
314 char port_str[16];
315
316 if (!address)
317 address = "0.0.0.0";
318
319 memset(&hints, 0, sizeof(hints));
320 hints.ai_family = AF_INET; // IPv4
321 hints.ai_socktype = SOCK_STREAM;
322 hints.ai_flags = AI_PASSIVE;
323
324 safe_snprintf(port_str, sizeof(port_str), "%d", port);
325
326 int s = getaddrinfo(address, port_str, &hints, &result);
327 if (s != 0) {
328 SET_ERRNO(ERROR_NETWORK, "getaddrinfo failed: %s", gai_strerror(s));
330 }
331
332 socket_t listen_sock = INVALID_SOCKET_VALUE;
333 for (rp = result; rp != NULL; rp = rp->ai_next) {
334 listen_sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
335 if (listen_sock == INVALID_SOCKET_VALUE) {
336 continue;
337 }
338
339 // Set SO_REUSEADDR to allow rebinding quickly after restart
340 int reuse = 1;
341 if (setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(reuse)) < 0) {
342 log_warn("setsockopt SO_REUSEADDR failed");
343 }
344
345 if (bind(listen_sock, rp->ai_addr, (int)rp->ai_addrlen) == 0) {
346 break; // Success
347 }
348
349 socket_close(listen_sock);
350 listen_sock = INVALID_SOCKET_VALUE;
351 }
352
353 freeaddrinfo(result);
354
355 if (listen_sock == INVALID_SOCKET_VALUE) {
356 SET_ERRNO_SYS(ERROR_NETWORK_BIND, "Failed to bind listen socket on %s:%d", address, port);
358 }
359
360 if (listen(listen_sock, SOMAXCONN) != 0) {
361 SET_ERRNO_SYS(ERROR_NETWORK_BIND, "listen() failed on %s:%d", address, port);
362 socket_close(listen_sock);
364 }
365
366 return listen_sock;
367}
368
377static void *accept_loop_thread(void *arg) {
378 session_host_t *host = (session_host_t *)arg;
379 if (!host)
380 return NULL;
381
382 log_info("Accept loop started");
383
384 // Use select() to handle accept with timeout
385 struct timeval tv;
386 fd_set readfds;
387 int max_fd;
388
389 while (host->accept_thread_running && host->running) {
390 // Set timeout to 1 second
391 tv.tv_sec = 0;
392 tv.tv_usec = 10000;
393
394 FD_ZERO(&readfds);
395 max_fd = 0;
396
397 if (host->socket_v4 != INVALID_SOCKET_VALUE) {
398 FD_SET(host->socket_v4, &readfds);
399 max_fd = (int)host->socket_v4;
400 }
401
402 // Wait for incoming connections
403 int activity = select(max_fd + 1, &readfds, NULL, NULL, &tv);
404 if (activity < 0) {
405 if (errno != EINTR) {
406 log_error("select() failed");
407 }
408 continue;
409 }
410
411 if (activity == 0) {
412 // Timeout - check if we should exit
413 continue;
414 }
415
416 // Check for incoming connections
417 if (host->socket_v4 != INVALID_SOCKET_VALUE && FD_ISSET(host->socket_v4, &readfds)) {
418 struct sockaddr_in client_addr;
419 socklen_t client_addr_len = sizeof(client_addr);
420
421 // Accept incoming connection
422 socket_t client_socket = accept(host->socket_v4, (struct sockaddr *)&client_addr, &client_addr_len);
423 if (client_socket == INVALID_SOCKET_VALUE) {
424 log_warn("accept() failed");
425 continue;
426 }
427
428 // Get client IP and port
429 char client_ip[64];
430 inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
431 int client_port = ntohs(client_addr.sin_port);
432
433 log_info("New connection from %s:%d", client_ip, client_port);
434
435 // Add client to host
436 uint32_t client_id = session_host_add_client(host, client_socket, client_ip, client_port);
437 if (client_id == 0) {
438 log_error("Failed to add client");
439 socket_close(client_socket);
440 }
441 }
442 }
443
444 log_info("Accept loop stopped");
445 return NULL;
446}
447
456static void *receive_loop_thread(void *arg) {
457 session_host_t *host = (session_host_t *)arg;
458 if (!host)
459 return NULL;
460
461 log_info("Receive loop started");
462
463 struct timeval tv;
464 fd_set readfds;
465 socket_t max_fd;
466
467 while (host->receive_thread_running && host->running) {
468 // Set timeout to 1 second
469 tv.tv_sec = 1;
470 tv.tv_usec = 0;
471
472 FD_ZERO(&readfds);
473 max_fd = INVALID_SOCKET_VALUE;
474
475 // Add all active client sockets to the set
477 for (int i = 0; i < host->max_clients; i++) {
478 if (host->clients[i].active && host->clients[i].socket != INVALID_SOCKET_VALUE) {
479 FD_SET(host->clients[i].socket, &readfds);
480 if (max_fd == INVALID_SOCKET_VALUE || host->clients[i].socket > max_fd) {
481 max_fd = host->clients[i].socket;
482 }
483 }
484 }
486
487 // If clients only have a non-socket transport, poll its receive queue below.
488 if (max_fd == INVALID_SOCKET_VALUE) {
490 }
491
492 // Wait briefly for socket data, then also check non-socket transports.
493 int activity = max_fd == INVALID_SOCKET_VALUE ? 0 : select((int)max_fd + 1, &readfds, NULL, NULL, &tv);
494 if (activity < 0) {
495 if (errno != EINTR) {
496 log_error("select() failed in receive loop");
497 }
498 continue;
499 }
500
501 // Check socket readiness and queued transport data for each client.
503 for (int i = 0; i < host->max_clients; i++) {
504 if (!host->clients[i].active) {
505 continue;
506 }
507
508 acip_transport_t *client_transport = host->clients[i].transport;
509 socket_t client_socket = host->clients[i].socket;
510 bool transport_ready = client_transport && acip_transport_has_pending_data(client_transport);
511 bool socket_ready = client_socket != INVALID_SOCKET_VALUE && FD_ISSET(client_socket, &readfds);
512 if (!transport_ready && !socket_ready) {
513 continue;
514 }
515
516 // Try to receive packet from this client
517 packet_type_t ptype = (packet_type_t)0;
518 void *data = NULL;
519 size_t len = 0;
520 void *transport_buffer = NULL;
521 size_t transport_buffer_len = 0;
522 asciichat_error_t result;
523 if (transport_ready) {
524 void *packet = NULL;
525 result = acip_transport_recv(client_transport, &packet, &transport_buffer_len, &transport_buffer);
526 if (result == ASCIICHAT_OK && packet && transport_buffer_len >= sizeof(packet_header_t)) {
527 const packet_header_t *header = (const packet_header_t *)packet;
528 ptype = (packet_type_t)NET_TO_HOST_U16(header->type);
529 len = NET_TO_HOST_U32(header->length);
530 if (len != transport_buffer_len - sizeof(packet_header_t)) {
531 result = SET_ERRNO(ERROR_NETWORK, "WebRTC packet length mismatch");
532 } else {
533 data = (uint8_t *)packet + sizeof(packet_header_t);
534 }
535 } else if (result == ASCIICHAT_OK) {
536 result = SET_ERRNO(ERROR_NETWORK, "WebRTC packet header is truncated");
537 }
538 } else {
539 result = packet_receive(client_socket, &ptype, &data, &len);
540 }
541
542 if (result != ASCIICHAT_OK) {
543 log_warn("Failed to receive packet from client %u: %d", host->clients[i].client_id, result);
544 if (transport_buffer) {
545 buffer_pool_free(NULL, transport_buffer, transport_buffer_len);
546 }
547 // Client disconnected or error - will be cleaned up by timeout mechanism
548 continue;
549 }
550
551 // Process packet based on type
552 uint32_t client_id = host->clients[i].client_id;
554
555 switch (ptype) {
557 // Client sent a video frame - parse and store in incoming buffer
558 if (data && len >= sizeof(image_frame_packet_t)) {
559 const image_frame_packet_t *frame_hdr = (const image_frame_packet_t *)data;
560 const uint8_t *pixel_data = (const uint8_t *)data + sizeof(image_frame_packet_t);
561 size_t pixel_data_size = len - sizeof(image_frame_packet_t);
562 uint32_t width = NET_TO_HOST_U32(frame_hdr->width);
563 uint32_t height = NET_TO_HOST_U32(frame_hdr->height);
564 uint32_t pixel_format = NET_TO_HOST_U32(frame_hdr->pixel_format);
565 size_t bytes_per_pixel = (pixel_format == PIXEL_FORMAT_RGBA || pixel_format == PIXEL_FORMAT_BGRA) ? 4 : 3;
566 size_t expected_size = 0;
567 size_t pixel_count = 0;
568
569 if (pixel_format < PIXEL_FORMAT_RGB || pixel_format > PIXEL_FORMAT_BGRA || width == 0 || height == 0 ||
570 width > INT_MAX || height > INT_MAX ||
571 checked_size_mul((size_t)width, (size_t)height, &pixel_count) != ASCIICHAT_OK ||
572 checked_size_mul(pixel_count, bytes_per_pixel, &expected_size) != ASCIICHAT_OK ||
573 expected_size != pixel_data_size) {
574 log_warn("Discarding malformed video frame from client %u (%ux%u, format %u, payload %zu)", client_id,
575 width, height, pixel_format, pixel_data_size);
576 } else {
577 // Frame dimensions and pixel format are network-order fields. Resize
578 // this participant's image when its capture dimensions change.
580 for (int j = 0; j < host->max_clients; j++) {
581 if (host->clients[j].active && host->clients[j].client_id == client_id &&
582 host->clients[j].incoming_video) {
583 image_t *img = host->clients[j].incoming_video;
584 if (img->w != (int)width || img->h != (int)height) {
585 image_t *resized = image_new(width, height);
586 if (!resized) {
587 log_warn("Could not allocate %ux%u video frame for client %u", width, height, client_id);
588 break;
589 }
590 image_destroy(img);
591 host->clients[j].incoming_video = img = resized;
592 }
593
594 uint8_t *dst = (uint8_t *)img->pixels;
595 if (pixel_format == PIXEL_FORMAT_RGB) {
596 memcpy(dst, pixel_data, pixel_count * 3);
597 } else if (pixel_format == PIXEL_FORMAT_BGR) {
598 for (size_t p = 0; p < pixel_count; p++) {
599 dst[p * 3] = pixel_data[p * 3 + 2];
600 dst[p * 3 + 1] = pixel_data[p * 3 + 1];
601 dst[p * 3 + 2] = pixel_data[p * 3];
602 }
603 } else {
604 const bool bgra = pixel_format == PIXEL_FORMAT_BGRA;
605 for (size_t p = 0; p < pixel_count; p++) {
606 const uint8_t *src = pixel_data + p * 4;
607 dst[p * 3] = src[bgra ? 2 : 0];
608 dst[p * 3 + 1] = src[1];
609 dst[p * 3 + 2] = src[bgra ? 0 : 2];
610 }
611 }
612 log_debug_every(500 * US_PER_MS_INT, "Frame received from client %u (%ux%u, format %u)", client_id,
613 width, height, pixel_format);
614 break;
615 }
616 }
618 }
619 }
620 break;
621
623 // Client sent Opus-encoded audio batch
624 if (data && len > 16) { // Must have at least header
625 const uint8_t *batch_data = (const uint8_t *)data;
626 // Parse header: sample_rate (4), frame_duration (4), frame_count (4), reserved (4)
627 (void)batch_data[0]; // Avoid unused variable warning if we don't use sample_rate/frame_duration
628 uint32_t batch_frame_count_net = 0;
629 memcpy(&batch_frame_count_net, batch_data + 8, sizeof(batch_frame_count_net));
630 uint32_t batch_frame_count = NET_TO_HOST_U32(batch_frame_count_net);
631
632 size_t frame_sizes_len = (size_t)batch_frame_count * sizeof(uint16_t);
633 if (batch_frame_count > 0 && batch_frame_count <= 1000 && 16 + frame_sizes_len <= len) {
634 const uint8_t *frame_sizes = batch_data + 16;
635 const uint8_t *opus_frames = batch_data + 16 + (batch_frame_count * sizeof(uint16_t));
636 const uint8_t *batch_end = batch_data + len;
637
638 // Find client and decode audio
640 for (int j = 0; j < host->max_clients; j++) {
641 if (host->clients[j].client_id == client_id && host->clients[j].incoming_audio && host->opus_decoder) {
642 // Decode each Opus frame and write to ringbuffer
643 const uint8_t *current_frame = opus_frames;
644 for (uint32_t k = 0; k < batch_frame_count; k++) {
645 uint16_t frame_size_net = 0;
646 memcpy(&frame_size_net, frame_sizes + k * sizeof(frame_size_net), sizeof(frame_size_net));
647 uint16_t frame_size = NET_TO_HOST_U16(frame_size_net);
648 if ((size_t)(batch_end - current_frame) < frame_size) {
649 break;
650 }
651 if (frame_size > 0) {
652 // Allocate buffer for decoded samples
653 float decoded_samples[960]; // Max 20ms @ 48kHz
654 int decoded_count =
655 opus_codec_decode(host->opus_decoder, current_frame, (int)frame_size, decoded_samples, 960);
656 if (decoded_count > 0) {
657 // Write samples to ringbuffer one at a time
658 for (int s = 0; s < decoded_count; s++) {
659 ringbuffer_write(host->clients[j].incoming_audio, &decoded_samples[s]);
660 }
661 }
662 }
663 current_frame += frame_size;
664 }
665 log_debug_every(NS_PER_MS_INT, "Audio batch received from client %u (%u frames)", client_id,
666 batch_frame_count);
667 break;
668 }
669 }
671 }
672 }
673 break;
674
676 log_info("Client %u started streaming", client_id);
678 for (int j = 0; j < host->max_clients; j++) {
679 if (host->clients[j].client_id == client_id) {
680 host->clients[j].video_active = true;
681 break;
682 }
683 }
685 break;
686
688 if (data && len >= sizeof(terminal_capabilities_packet_t)) {
690 memcpy(&caps, data, sizeof(caps));
691 uint16_t width = NET_TO_HOST_U16(caps.width);
692 uint16_t height = NET_TO_HOST_U16(caps.height);
693 if (width > 0 && height > 0) {
695 for (int j = 0; j < host->max_clients; j++) {
696 if (host->clients[j].active && host->clients[j].client_id == client_id) {
697 host->clients[j].render_width = width;
698 host->clients[j].render_height = height;
699 log_info("Client %u requested render dimensions %ux%u", client_id, width, height);
700 break;
701 }
702 }
704 }
705 }
706 break;
707
709 log_info("Client %u stopped streaming", client_id);
711 for (int j = 0; j < host->max_clients; j++) {
712 if (host->clients[j].client_id == client_id) {
713 host->clients[j].video_active = false;
714 break;
715 }
716 }
718 break;
719
720 case PACKET_TYPE_PING:
721 // Respond with PONG
722 log_debug_every(NS_PER_MS_INT, "PING from client %u", client_id);
723 if (client_transport) {
724 packet_send_via_transport(client_transport, PACKET_TYPE_PONG, NULL, 0, 0);
725 } else {
726 packet_send(client_socket, PACKET_TYPE_PONG, NULL, 0);
727 }
728 break;
729
731 log_info("Client %u requested disconnect", client_id);
732 // Mark for removal (will be handled separately to avoid deadlock)
733 break;
734
735 default:
736 log_warn("Unknown packet type %u from client %u", ptype, client_id);
737 break;
738 }
739
740 // Free packet data
741 if (transport_buffer) {
742 buffer_pool_free(NULL, transport_buffer, transport_buffer_len);
743 } else if (data) {
744 SAFE_FREE(data);
745 }
746
748 }
750 }
751
752 log_info("Receive loop stopped");
753 return NULL;
754}
755
765static void *host_render_thread(void *arg) {
766 session_host_t *host = (session_host_t *)arg;
767 if (!host) {
768 SET_ERRNO(ERROR_INVALID_PARAM, "host_render_thread: invalid host");
769 return NULL;
770 }
771
772 log_info("Host render thread started");
773
774 uint64_t last_video_render_ns = 0;
775 uint64_t last_audio_render_ns = 0;
776
777 while (host->render_thread_running && host->running) {
778 uint64_t now_ns = time_get_ns();
779
780 // VIDEO RENDERING (60 FPS = 16.7ms)
781 if (time_elapsed_ns(last_video_render_ns, now_ns) >= NS_PER_MS_INT * 16) {
782 // Collect video frames from all participants and create grid layout
784
785 // Count active participants with video
786 int active_video_count = 0;
787 for (int i = 0; i < host->max_clients; i++) {
788 if (host->clients[i].active && host->clients[i].video_active && host->clients[i].incoming_video) {
789 active_video_count++;
790 }
791 }
792
793 // Generate and broadcast grid if we have active participants,
794 // or display locally if display context is set
795 if (active_video_count > 0 || host->display_context) {
796 // Allocate arrays for ASCII frames and sources
797 char **ascii_frames = SAFE_MALLOC(active_video_count * sizeof(char *), char **);
798 ascii_frame_source_t *sources =
799 SAFE_MALLOC(active_video_count * sizeof(ascii_frame_source_t), ascii_frame_source_t *);
800
801 if (ascii_frames && sources) {
802 // Convert each incoming video frame to ASCII
803 int frame_idx = 0;
804 for (int i = 0; i < host->max_clients; i++) {
805 if (host->clients[i].active && host->clients[i].video_active && host->clients[i].incoming_video) {
806 image_t *img = host->clients[i].incoming_video;
807
808 // Convert image to ASCII (80x24 for each frame in grid, monochrome for now)
809 ascii_frames[frame_idx] =
810 ascii_convert(img, 80, 24, true, false, false, PALETTE_CHARS_STANDARD, g_default_luminance_palette);
811 if (ascii_frames[frame_idx]) {
812 sources[frame_idx].frame_data = ascii_frames[frame_idx];
813 sources[frame_idx].frame_size = strlen(ascii_frames[frame_idx]) + 1;
814 } else {
815 sources[frame_idx].frame_data = "";
816 sources[frame_idx].frame_size = 1;
817 }
818 frame_idx++;
819 }
820 }
821
822 // Use the first connected WebRTC client's negotiated dimensions for
823 // the shared grid so its ACIP frame dimensions match the browser renderer.
824 int output_width = 80;
825 int output_height = 24;
826 for (int i = 0; i < host->max_clients; i++) {
827 if (host->clients[i].active && host->clients[i].transport && host->clients[i].render_width > 0 &&
828 host->clients[i].render_height > 0) {
829 output_width = host->clients[i].render_width;
830 output_height = host->clients[i].render_height;
831 break;
832 }
833 }
834 size_t grid_size = 0;
835 char *grid_frame = ascii_create_grid(sources, active_video_count, output_width, output_height, &grid_size);
836
837 if (grid_frame) {
838 // Broadcast grid to all connected participants
839 if (active_video_count > 0) {
840 for (int i = 0; i < host->max_clients; i++) {
841 if (host->clients[i].active &&
842 (host->clients[i].transport || host->clients[i].socket != INVALID_SOCKET_VALUE)) {
843 if (host->clients[i].transport) {
844 // Web clients parse the ACIP ASCII-frame payload, which includes
845 // dimensions, size, and checksum ahead of the grid bytes.
846 uint16_t client_width = host->clients[i].render_width;
847 uint16_t client_height = host->clients[i].render_height;
848 if (client_width == 0 || client_height == 0) {
849 client_width = (uint16_t)output_width;
850 client_height = (uint16_t)output_height;
851 }
852
853 if (active_video_count != 1 && client_width == output_width && client_height == output_height) {
854 acip_send_ascii_frame(host->clients[i].transport, grid_frame, grid_size, output_width,
855 output_height, "session-host");
856 } else {
857 // The terminal renderer's line feeds rely on the grid
858 // width. Rebuild the source cells at this client's grid
859 // dimensions instead of relabeling an 80x24 frame.
860 char **client_frames = SAFE_CALLOC((size_t)active_video_count, sizeof(*client_frames), char **);
861 ascii_frame_source_t *client_sources =
862 SAFE_CALLOC((size_t)active_video_count, sizeof(*client_sources), ascii_frame_source_t *);
863 if (client_frames && client_sources) {
864 int client_frame_idx = 0;
865 for (int j = 0; j < host->max_clients; j++) {
866 if (host->clients[j].active && host->clients[j].video_active &&
867 host->clients[j].incoming_video) {
868 client_frames[client_frame_idx] =
869 ascii_convert(host->clients[j].incoming_video, client_width, client_height, true, false,
871 client_sources[client_frame_idx].frame_data =
872 client_frames[client_frame_idx] ? client_frames[client_frame_idx] : "";
873 client_sources[client_frame_idx].frame_size =
874 client_frames[client_frame_idx] ? strlen(client_frames[client_frame_idx]) + 1 : 1;
875 client_frame_idx++;
876 }
877 }
878 if (active_video_count == 1 && client_frames[0]) {
879 acip_send_ascii_frame(host->clients[i].transport, client_frames[0], strlen(client_frames[0]),
880 client_width, client_height, "session-host");
881 } else {
882 size_t client_grid_size = 0;
883 char *client_grid = ascii_create_grid(client_sources, active_video_count, client_width,
884 client_height, &client_grid_size);
885 if (client_grid) {
886 acip_send_ascii_frame(host->clients[i].transport, client_grid, client_grid_size,
887 client_width, client_height, "session-host");
888 SAFE_FREE(client_grid);
889 }
890 }
891 for (int j = 0; j < active_video_count; j++) {
892 SAFE_FREE(client_frames[j]);
893 }
894 }
895 SAFE_FREE(client_frames);
896 SAFE_FREE(client_sources);
897 }
898 } else {
899 session_host_send_packet(&host->clients[i], PACKET_TYPE_ASCII_FRAME, grid_frame, grid_size);
900 }
901 }
902 }
903 }
904
905 // Display locally if display context is set
906 if (host->display_context) {
907 // Disable terminal logging on first rendered frame so logs don't
908 // interleave with ASCII art output
909 static bool logging_disabled = false;
910 if (!logging_disabled) {
912 logging_disabled = true;
913 }
915 }
916
917 SAFE_FREE(grid_frame);
918 }
919
920 // Free ASCII frames
921 for (int i = 0; i < active_video_count; i++) {
922 if (ascii_frames[i]) {
923 SAFE_FREE(ascii_frames[i]);
924 }
925 }
926 }
927
928 SAFE_FREE(ascii_frames);
929 SAFE_FREE(sources);
930 }
931
933 log_debug_every(NS_PER_MS_INT, "Video render cycle (%d active)", active_video_count);
934 last_video_render_ns = now_ns;
935 }
936
937 // AUDIO RENDERING (100 FPS = 10ms)
938 if (time_elapsed_ns(last_audio_render_ns, now_ns) >= NS_PER_MS_INT * 10) {
939 // Mix audio from all participants
940 // 1. Read samples from each participant's incoming_audio ringbuffer
941 // 2. Mix into output buffer (simple addition with clipping)
942 // 3. Encode with Opus
943 // 4. Broadcast mixed audio via av_send_audio_opus_batch()
944
945 if (host->audio_ctx && host->opus_encoder) {
946 float mixed_audio[960]; // 20ms @ 48kHz
947 memset(mixed_audio, 0, sizeof(mixed_audio));
948
949 // Lock clients mutex to safely read audio buffers
951 for (int i = 0; i < host->max_clients; i++) {
952 if (!host->clients[i].active || !host->clients[i].audio_active) {
953 continue;
954 }
955
956 if (!host->clients[i].incoming_audio) {
957 continue;
958 }
959
960 // Read audio samples one-by-one from this participant's ringbuffer
961 // ringbuffer_read() reads one element (1 float) per call
962 for (int j = 0; j < 960; j++) {
963 float sample = 0.0f;
964 if (ringbuffer_read(host->clients[i].incoming_audio, &sample)) {
965 // Successfully read a sample
966 mixed_audio[j] += sample;
967 // Clip to [-1.0, 1.0] to prevent distortion
968 if (mixed_audio[j] > 1.0f) {
969 mixed_audio[j] = 1.0f;
970 } else if (mixed_audio[j] < -1.0f) {
971 mixed_audio[j] = -1.0f;
972 }
973 }
974 // If ringbuffer is empty, sample remains 0.0f and that's fine
975 }
976 }
978
980
981 // Encode to Opus
982 uint8_t opus_buffer[1000];
983 size_t opus_len = opus_codec_encode(host->opus_encoder, mixed_audio, 960, opus_buffer, sizeof(opus_buffer));
984
985 // Only broadcast audio if we have network-connected clients (not memory participants)
986 if (opus_len > 0) {
987 uint16_t frame_sizes[1] = {(uint16_t)opus_len};
989 bool has_socket_clients = false;
990 for (int i = 0; i < host->max_clients; i++) {
991 if (!host->clients[i].active) {
992 continue;
993 }
994 if (host->clients[i].transport) {
995 asciichat_error_t audio_result = acip_send_audio_opus_batch(host->clients[i].transport, opus_buffer,
996 opus_len, frame_sizes, 1, 48000, 20);
997 if (audio_result != ASCIICHAT_OK) {
998 log_warn("Failed to send mixed audio to WebRTC client %u", host->clients[i].client_id);
999 }
1000 } else if (host->clients[i].socket != INVALID_SOCKET_VALUE) {
1001 has_socket_clients = true;
1002 }
1003 }
1005
1006 if (has_socket_clients) {
1007 // Preserve the existing socket-based audio broadcast path.
1008 av_send_audio_opus_batch(host->socket_v4, opus_buffer, opus_len, frame_sizes, 48000, 20, 1, NULL);
1009 }
1010 }
1011 }
1012
1013 log_debug_every(NS_PER_MS_INT, "Audio render cycle");
1014 last_audio_render_ns = now_ns;
1015 }
1016
1017 // Small sleep to prevent busy-loop
1018 APP_CALLBACK_VOID(platform_pump_events);
1020 }
1021
1022 log_info("Host render thread stopped");
1023 return NULL;
1024}
1025
1027 if (!host || !host->initialized) {
1028 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_start: invalid host");
1029 }
1030
1031 if (host->running) {
1032 return ASCIICHAT_OK; // Already running
1033 }
1034
1035 // Create listen socket(s)
1036 // For simplicity, we bind to IPv4 if specified, otherwise default to 0.0.0.0
1037 const char *bind_address = host->ipv4_address[0] ? host->ipv4_address : "0.0.0.0";
1038
1039 host->socket_v4 = create_listen_socket(bind_address, host->port);
1040 if (host->socket_v4 == INVALID_SOCKET_VALUE) {
1041 log_error("Failed to create IPv4 listen socket");
1042 if (host->callbacks.on_error) {
1043 host->callbacks.on_error(host, ERROR_NETWORK_BIND, "Failed to create listen socket", host->user_data);
1044 }
1045 return GET_ERRNO();
1046 }
1047
1048 host->running = true;
1049 log_info("Session host listening on %s:%d", bind_address, host->port);
1050
1051 // Spawn accept loop thread
1052 host->accept_thread_running = true;
1053 if (asciichat_thread_create(&host->accept_thread, "accept", accept_loop_thread, host) != 0) {
1054 log_error("Failed to spawn accept loop thread");
1055 host->accept_thread_running = false;
1056 if (host->callbacks.on_error) {
1057 host->callbacks.on_error(host, ERROR_THREAD, "Failed to spawn accept loop thread", host->user_data);
1058 }
1059 socket_close(host->socket_v4);
1061 host->running = false;
1062 return SET_ERRNO(ERROR_THREAD, "Failed to spawn accept loop thread");
1063 }
1064
1065 // Spawn receive loop thread
1066 host->receive_thread_running = true;
1067 if (asciichat_thread_create(&host->receive_thread, "host_recv", receive_loop_thread, host) != 0) {
1068 log_error("Failed to spawn receive loop thread");
1069 host->receive_thread_running = false;
1070 host->accept_thread_running = false;
1072 if (host->callbacks.on_error) {
1073 host->callbacks.on_error(host, ERROR_THREAD, "Failed to spawn receive loop thread", host->user_data);
1074 }
1075 socket_close(host->socket_v4);
1077 host->running = false;
1078 return SET_ERRNO(ERROR_THREAD, "Failed to spawn receive loop thread");
1079 }
1080
1081 // Spawn render thread (optional - can be started later)
1082 // This thread handles video mixing and audio distribution
1083 return ASCIICHAT_OK;
1084}
1085
1087 if (!host || !host->initialized || !host->running) {
1088 return;
1089 }
1090
1091 // Stop render thread if running
1092 if (host->render_thread_running) {
1093 host->render_thread_running = false;
1095 log_info("Render thread joined");
1096 }
1097
1098 // Stop receive loop thread (reads from client sockets)
1099 if (host->receive_thread_running) {
1100 host->receive_thread_running = false;
1102 log_info("Receive loop thread joined");
1103 }
1104
1105 // Stop accept loop thread (before closing listen socket)
1106 if (host->accept_thread_running) {
1107 host->accept_thread_running = false;
1109 log_info("Accept loop thread joined");
1110 }
1111
1112 // Disconnect all clients
1113 mutex_lock(&host->clients_mutex);
1114 for (int i = 0; i < host->max_clients; i++) {
1115 if (host->clients[i].active) {
1116 // Invoke callback
1117 if (host->callbacks.on_client_leave) {
1118 host->callbacks.on_client_leave(host, host->clients[i].client_id, host->user_data);
1119 }
1120
1121 // Close socket
1122 if (host->clients[i].socket != INVALID_SOCKET_VALUE) {
1123 socket_close(host->clients[i].socket);
1125 }
1126
1127 host->clients[i].active = false;
1128 }
1129 }
1130 host->client_count = 0;
1132
1133 // Close listen sockets
1134 if (host->socket_v4 != INVALID_SOCKET_VALUE) {
1135 socket_close(host->socket_v4);
1137 }
1138 if (host->socket_v6 != INVALID_SOCKET_VALUE) {
1139 socket_close(host->socket_v6);
1141 }
1142
1143 host->running = false;
1144}
1145
1147 if (!host || !host->initialized) {
1148 return false;
1149 }
1150 return host->running;
1151}
1152
1153/* ============================================================================
1154 * Session Host Client Management Functions
1155 * ============================================================================ */
1156
1157uint32_t session_host_add_client(session_host_t *host, socket_t socket, const char *ip, int port) {
1158 if (!host || !host->initialized) {
1159 return 0;
1160 }
1161
1162 mutex_lock(&host->clients_mutex);
1163
1164 // Check if we have room
1165 if (host->client_count >= host->max_clients) {
1167 SET_ERRNO(ERROR_SESSION_FULL, "Maximum clients reached");
1168 return 0;
1169 }
1170
1171 // Find empty slot
1172 for (int i = 0; i < host->max_clients; i++) {
1173 if (!host->clients[i].active) {
1175 host->clients[i].client_id = host->next_client_id++;
1176 host->clients[i].socket = socket;
1177 if (ip) {
1178 SAFE_STRNCPY(host->clients[i].ip_address, ip, sizeof(host->clients[i].ip_address));
1179 }
1180 host->clients[i].port = port;
1181 host->clients[i].active = true;
1182 host->clients[i].video_active = false;
1183 host->clients[i].audio_active = false;
1184 host->clients[i].connected_at = (uint64_t)time(NULL);
1185 host->clients[i].transport = NULL; // No alternative transport initially
1186 host->clients[i].render_width = 0;
1187 host->clients[i].render_height = 0;
1188
1189 // Allocate media buffers
1190 host->clients[i].incoming_video = image_new(480, 270); // Network-optimal size (HD preview)
1191 host->clients[i].incoming_audio = ringbuffer_create(sizeof(float), 960 * 10); // ~200ms buffer @ 48kHz
1192
1193 if (!host->clients[i].incoming_video || !host->clients[i].incoming_audio) {
1194 // Cleanup on allocation failure
1195 if (host->clients[i].incoming_video) {
1197 host->clients[i].incoming_video = NULL;
1198 }
1199 if (host->clients[i].incoming_audio) {
1201 host->clients[i].incoming_audio = NULL;
1202 }
1203 host->clients[i].active = false;
1205 SET_ERRNO(ERROR_MEMORY, "Failed to allocate media buffers for client");
1206 return 0;
1207 }
1208
1209 uint32_t client_id = host->clients[i].client_id;
1210 host->client_count++;
1211
1213
1214 // Invoke callback
1215 if (host->callbacks.on_client_join) {
1216 host->callbacks.on_client_join(host, client_id, host->user_data);
1217 }
1218
1219 return client_id;
1220 }
1221 }
1222
1224 return 0;
1225}
1226
1228 if (!host || !host->initialized) {
1229 return 0;
1230 }
1231
1232 mutex_lock(&host->clients_mutex);
1233
1234 // Check if we have room
1235 if (host->client_count >= host->max_clients) {
1237 SET_ERRNO(ERROR_SESSION_FULL, "Maximum clients reached");
1238 return 0;
1239 }
1240
1241 // Check if memory participant already exists (only one allowed)
1242 for (int i = 0; i < host->max_clients; i++) {
1243 if (host->clients[i].active && host->clients[i].participant_type == PARTICIPANT_TYPE_MEMORY) {
1245 SET_ERRNO(ERROR_INVALID_PARAM, "Memory participant already exists");
1246 return 0;
1247 }
1248 }
1249
1250 // Find empty slot
1251 for (int i = 0; i < host->max_clients; i++) {
1252 if (!host->clients[i].active) {
1254 host->clients[i].client_id = host->next_client_id++;
1255 host->clients[i].socket = INVALID_SOCKET_VALUE; // No socket for memory participant
1256 SAFE_STRNCPY(host->clients[i].ip_address, "memory", sizeof(host->clients[i].ip_address));
1257 host->clients[i].port = 0;
1258 host->clients[i].active = true;
1259 host->clients[i].video_active = true; // Memory participants always have active video/audio
1260 host->clients[i].audio_active = true;
1261 host->clients[i].connected_at = (uint64_t)time(NULL);
1262 host->clients[i].transport = NULL;
1263
1264 // Allocate media buffers (same as network clients)
1265 host->clients[i].incoming_video = image_new(480, 270);
1266 host->clients[i].incoming_audio = ringbuffer_create(sizeof(float), 960 * 10);
1267
1268 if (!host->clients[i].incoming_video || !host->clients[i].incoming_audio) {
1269 if (host->clients[i].incoming_video) {
1271 host->clients[i].incoming_video = NULL;
1272 }
1273 if (host->clients[i].incoming_audio) {
1275 host->clients[i].incoming_audio = NULL;
1276 }
1277 host->clients[i].active = false;
1279 SET_ERRNO(ERROR_MEMORY, "Failed to allocate media buffers for memory participant");
1280 return 0;
1281 }
1282
1284 host->client_count++;
1285
1287
1288 log_info("Added memory participant with ID %u", participant_id);
1289
1290 // Invoke callback
1291 if (host->callbacks.on_client_join) {
1293 }
1294
1295 return participant_id;
1296 }
1297 }
1298
1300 return 0;
1301}
1302
1304 if (!host || !host->initialized || !frame) {
1305 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_inject_frame: invalid parameters");
1306 }
1307
1308 mutex_lock(&host->clients_mutex);
1309
1310 // Find memory participant
1311 for (int i = 0; i < host->max_clients; i++) {
1312 if (host->clients[i].active && host->clients[i].client_id == participant_id &&
1314
1315 if (!host->clients[i].incoming_video) {
1317 return SET_ERRNO(ERROR_INVALID_STATE, "Memory participant has no video buffer");
1318 }
1319
1320 // Copy frame data into buffer
1321 image_t *dest = host->clients[i].incoming_video;
1322 if (dest->w != frame->w || dest->h != frame->h) {
1323 // Reallocate if size changed
1324 image_destroy(dest);
1325 dest = image_new(frame->w, frame->h);
1326 if (!dest) {
1327 host->clients[i].incoming_video = NULL;
1329 return SET_ERRNO(ERROR_MEMORY, "Failed to reallocate video buffer");
1330 }
1331 host->clients[i].incoming_video = dest;
1332 }
1333
1334 // Copy pixel data
1335 memcpy(dest->pixels, frame->pixels, frame->w * frame->h * sizeof(rgb_pixel_t));
1336
1337 host->clients[i].video_active = true;
1338
1340 return ASCIICHAT_OK;
1341 }
1342 }
1343
1345 return SET_ERRNO(ERROR_NOT_FOUND, "Memory participant not found");
1346}
1347
1349 size_t count) {
1350 if (!host || !host->initialized || !samples || count == 0) {
1351 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_inject_audio: invalid parameters");
1352 }
1353
1354 mutex_lock(&host->clients_mutex);
1355
1356 // Find memory participant
1357 for (int i = 0; i < host->max_clients; i++) {
1358 if (host->clients[i].active && host->clients[i].client_id == participant_id &&
1360
1361 if (!host->clients[i].incoming_audio) {
1363 return SET_ERRNO(ERROR_INVALID_STATE, "Memory participant has no audio buffer");
1364 }
1365
1366 // Write samples to ringbuffer (one at a time)
1367 size_t written = 0;
1368 for (size_t j = 0; j < count; j++) {
1369 if (ringbuffer_write(host->clients[i].incoming_audio, &samples[j])) {
1370 written++;
1371 } else {
1372 break; // Buffer full
1373 }
1374 }
1375
1376 if (written < count) {
1377 log_warn_every(NS_PER_MS_INT, "Audio ringbuffer full, dropped %zu samples", count - written);
1378 }
1379
1380 host->clients[i].audio_active = true;
1381
1383 return ASCIICHAT_OK;
1384 }
1385 }
1386
1388 return SET_ERRNO(ERROR_NOT_FOUND, "Memory participant not found");
1389}
1390
1392 if (!host || !host->initialized) {
1393 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_remove_client: invalid host");
1394 }
1395
1396 mutex_lock(&host->clients_mutex);
1397
1398 for (int i = 0; i < host->max_clients; i++) {
1399 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1400 // Invoke callback before removing
1402 if (host->callbacks.on_client_leave) {
1403 host->callbacks.on_client_leave(host, client_id, host->user_data);
1404 }
1405 mutex_lock(&host->clients_mutex);
1406
1407 // Close socket
1408 if (host->clients[i].socket != INVALID_SOCKET_VALUE) {
1409 socket_close(host->clients[i].socket);
1411 }
1412
1413 // Clean up media buffers
1414 if (host->clients[i].incoming_video) {
1416 host->clients[i].incoming_video = NULL;
1417 }
1418 if (host->clients[i].incoming_audio) {
1420 host->clients[i].incoming_audio = NULL;
1421 }
1422
1423 host->clients[i].active = false;
1424 host->client_count--;
1425
1427 return ASCIICHAT_OK;
1428 }
1429 }
1430
1432 return SET_ERRNO(ERROR_NOT_FOUND, "Client not found: %u", client_id);
1433}
1434
1436 if (!host || !host->initialized || !info) {
1437 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_find_client: invalid parameter");
1438 }
1439
1440 mutex_lock(&host->clients_mutex);
1441
1442 for (int i = 0; i < host->max_clients; i++) {
1443 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1444 info->client_id = host->clients[i].client_id;
1445 SAFE_STRNCPY(info->ip_address, host->clients[i].ip_address, sizeof(info->ip_address));
1446 info->port = host->clients[i].port;
1447 info->video_active = host->clients[i].video_active;
1448 info->audio_active = host->clients[i].audio_active;
1449 info->connected_at = host->clients[i].connected_at;
1450
1452 return ASCIICHAT_OK;
1453 }
1454 }
1455
1457 return SET_ERRNO(ERROR_NOT_FOUND, "Client not found: %u", client_id);
1458}
1459
1461 if (!host || !host->initialized) {
1462 return 0;
1463 }
1464 return host->client_count;
1465}
1466
1468 if (!host || !host->initialized || !ids || max_ids <= 0) {
1469 return 0;
1470 }
1471
1472 mutex_lock(&host->clients_mutex);
1473
1474 int count = 0;
1475 for (int i = 0; i < host->max_clients && count < max_ids; i++) {
1476 if (host->clients[i].active) {
1477 ids[count++] = host->clients[i].client_id;
1478 }
1479 }
1480
1482 return count;
1483}
1484
1485/* ============================================================================
1486 * Session Host Broadcast Functions
1487 * ============================================================================ */
1488
1490 if (!host || !host->initialized || !frame) {
1491 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_broadcast_frame: invalid parameter");
1492 }
1493
1494 if (!host->running) {
1495 return SET_ERRNO(ERROR_INVALID_STATE, "session_host_broadcast_frame: not running");
1496 }
1497
1498 // Broadcast ASCII frame to all connected clients
1499 size_t frame_len = strlen(frame) + 1; // Include null terminator
1501
1502 mutex_lock(&host->clients_mutex);
1503 for (int i = 0; i < host->max_clients; i++) {
1504 if (host->clients[i].active && host->clients[i].transport) {
1505 asciichat_error_t send_result =
1506 acip_send_ascii_frame(host->clients[i].transport, frame, frame_len - 1, 80, 24, "session-host");
1507 if (send_result != ASCIICHAT_OK) {
1508 log_warn("Failed to send ASCII frame to client %u", host->clients[i].client_id);
1509 result = send_result;
1510 }
1511 } else if (host->clients[i].active && host->clients[i].socket != INVALID_SOCKET_VALUE) {
1512 asciichat_error_t send_result = packet_send(host->clients[i].socket, PACKET_TYPE_ASCII_FRAME, frame, frame_len);
1513 if (send_result != ASCIICHAT_OK) {
1514 log_warn("Failed to send ASCII frame to client %u", host->clients[i].client_id);
1515 result = send_result; // Store error but continue broadcasting to other clients
1516 }
1517 }
1518 }
1520
1521 return result;
1522}
1523
1525 if (!host || !host->initialized || !frame) {
1526 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_send_frame: invalid parameter");
1527 }
1528
1529 if (!host->running) {
1530 return SET_ERRNO(ERROR_INVALID_STATE, "session_host_send_frame: not running");
1531 }
1532
1533 // Send ASCII frame to specific client
1534 size_t frame_len = strlen(frame) + 1; // Include null terminator
1535
1536 mutex_lock(&host->clients_mutex);
1537 for (int i = 0; i < host->max_clients; i++) {
1538 if (host->clients[i].client_id == client_id && host->clients[i].active) {
1539 asciichat_error_t result =
1540 host->clients[i].transport
1541 ? acip_send_ascii_frame(host->clients[i].transport, frame, frame_len - 1, 80, 24, "session-host")
1542 : (host->clients[i].socket != INVALID_SOCKET_VALUE
1543 ? packet_send(host->clients[i].socket, PACKET_TYPE_ASCII_FRAME, frame, frame_len)
1544 : ERROR_NOT_FOUND);
1546 return result;
1547 }
1548 }
1550
1551 return SET_ERRNO(ERROR_NOT_FOUND, "session_host_send_frame: client %u not found", client_id);
1552}
1553
1554/* ============================================================================
1555 * Session Host Display Functions
1556 * ============================================================================ */
1557
1559 if (!host || !host->initialized) {
1560 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_set_display: invalid host");
1561 }
1562
1563 host->display_context = display;
1564 return ASCIICHAT_OK;
1565}
1566
1567/* ============================================================================
1568 * Session Host Render Thread Functions
1569 * ============================================================================ */
1570
1572 if (!host || !host->initialized) {
1573 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_start_render: invalid host");
1574 }
1575
1576 // Discovery mode starts the shared host renderer without passing through
1577 // server/main.c, which normally initializes the ASCII lookup palettes.
1579
1580 if (!host->running) {
1581 return SET_ERRNO(ERROR_INVALID_STATE, "session_host_start_render: not running");
1582 }
1583
1584 if (host->render_thread_running) {
1585 return ASCIICHAT_OK; // Already running
1586 }
1587
1588 // Create audio context for mixing (host mode = true)
1589 if (!host->audio_ctx) {
1590 host->audio_ctx = session_audio_create(true);
1591 if (!host->audio_ctx) {
1592 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to create audio context");
1593 }
1594 }
1595
1596 // Create Opus decoder for decoding incoming Opus audio (48kHz)
1597 if (!host->opus_decoder) {
1599 if (!host->opus_decoder) {
1601 host->audio_ctx = NULL;
1602 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to create Opus decoder");
1603 }
1604 }
1605
1606 // Create Opus encoder for encoding mixed audio for broadcast (48kHz, VOIP mode, 24kbps)
1607 if (!host->opus_encoder) {
1609 if (!host->opus_encoder) {
1611 host->opus_decoder = NULL;
1613 host->audio_ctx = NULL;
1614 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to create Opus encoder");
1615 }
1616 }
1617
1618 // Spawn render thread
1619 host->render_thread_running = true;
1620 if (asciichat_thread_create(&host->render_thread, "render", host_render_thread, host) != 0) {
1621 log_error("Failed to spawn render thread");
1622 host->render_thread_running = false;
1623 return SET_ERRNO(ERROR_THREAD, "Failed to spawn render thread");
1624 }
1625
1626 log_info("Host render thread started");
1627 return ASCIICHAT_OK;
1628}
1629
1631 if (!host || !host->initialized) {
1632 return;
1633 }
1634
1635 if (!host->render_thread_running) {
1636 return;
1637 }
1638
1639 // Signal thread to stop
1640 host->render_thread_running = false;
1641
1642 // Wait for thread to complete
1644
1645 // Clean up audio resources
1646 if (host->audio_ctx) {
1648 host->audio_ctx = NULL;
1649 }
1650 if (host->opus_decoder) {
1652 host->opus_decoder = NULL;
1653 }
1654 if (host->opus_encoder) {
1656 host->opus_encoder = NULL;
1657 }
1658
1659 log_info("Host render thread stopped");
1660}
1661
1662/* ============================================================================
1663 * Session Host Transport Functions (WebRTC Integration)
1664 * ============================================================================ */
1665
1667 acip_transport_t *transport) {
1668 if (!host || !host->initialized) {
1669 return SET_ERRNO(ERROR_INVALID_PARAM, "Host is NULL or not initialized");
1670 }
1671
1672 mutex_lock(&host->clients_mutex);
1673
1674 // Find client with matching ID
1675 for (int i = 0; i < host->max_clients; i++) {
1676 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1677 log_info("session_host_set_client_transport: Setting transport=%p for client %u (was=%p)", transport, client_id,
1678 host->clients[i].transport);
1679
1680 host->clients[i].transport = transport;
1681
1682 if (transport) {
1683 log_info("WebRTC transport now active for client %u", client_id);
1684 } else {
1685 log_info("WebRTC transport cleared for client %u, reverting to socket", client_id);
1686 }
1687
1688 server_state_packet_t state = {0};
1689 if (transport) {
1690 uint32_t active_count = 0;
1691 for (int j = 0; j < host->max_clients; j++) {
1692 if (host->clients[j].active) {
1693 active_count++;
1694 }
1695 }
1696 state.connected_client_count = HOST_TO_NET_U32(active_count);
1697 state.active_client_count = HOST_TO_NET_U32(active_count);
1698 }
1700
1701 if (transport) {
1702 // Discovery sessions bypass the native server handshake. Notify the
1703 // browser after registering its transport so it can start media.
1704 asciichat_error_t send_result = acip_send_server_state(transport, &state);
1705 if (send_result != ASCIICHAT_OK) {
1706 return send_result;
1707 }
1708 }
1709 return ASCIICHAT_OK;
1710 }
1711 }
1712
1714 log_warn("Client %u not found", client_id);
1715 return SET_ERRNO(ERROR_NOT_FOUND, "Client not found");
1716}
1717
1719 if (!host || !host->initialized) {
1720 return NULL;
1721 }
1722
1723 mutex_lock(&host->clients_mutex);
1724
1725 // Find client with matching ID
1726 for (int i = 0; i < host->max_clients; i++) {
1727 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1728 acip_transport_t *transport = host->clients[i].transport;
1730 return transport;
1731 }
1732 }
1733
1735 return NULL;
1736}
1737
1739 if (!host || !host->initialized) {
1740 return false;
1741 }
1742
1743 mutex_lock(&host->clients_mutex);
1744
1745 // Find client with matching ID
1746 for (int i = 0; i < host->max_clients; i++) {
1747 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1748 bool has_transport = host->clients[i].transport != NULL;
1750 return has_transport;
1751 }
1752 }
1753
1755 return false;
1756}
Application-level callbacks for library code.
#define APP_CALLBACK_VOID(callback_name)
โš ๏ธโ€ผ๏ธ Comprehensive thread-local error context system for ascii-chat
โš™๏ธ Common definitions, error codes, macros, and types shared throughout the application
๐Ÿ”„ Network byte order conversion helpers
#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
Cross-platform error handling utilities.
opus_codec_t * opus_codec_create_decoder(int sample_rate)
Create an Opus decoder.
Definition opus.c:74
int opus_codec_decode(opus_codec_t *codec, const uint8_t *data, size_t data_len, float *out_samples, int out_num_samples)
Decode Opus audio frame.
Definition opus.c:143
opus_codec_t * opus_codec_create_encoder(opus_application_t application, int sample_rate, int bitrate)
Create an Opus encoder.
Definition opus.c:19
void opus_codec_destroy(opus_codec_t *codec)
Destroy an Opus codec instance.
Definition opus.c:230
size_t opus_codec_encode(opus_codec_t *codec, const float *samples, int num_samples, uint8_t *out_data, size_t out_size)
Encode audio frame with Opus.
Definition opus.c:112
@ OPUS_APPLICATION_VOIP
Voice over IP (optimized for speech)
Definition opus.h:77
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
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define SET_ERRNO_SYS(code, context_msg,...)
Set error code with custom message and system error context, returning the error code.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
#define GET_ERRNO()
Get current error code (0 if no error)
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_INVALID_STATE
@ ERROR_NETWORK_BIND
Definition error_codes.h:78
@ ERROR_NETWORK
Definition error_codes.h:77
@ ERROR_NOT_FOUND
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_SESSION_FULL
Definition error_codes.h:87
@ ERROR_INVALID_PARAM
@ ERROR_THREAD
#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_info(...)
Log an INFO message.
Definition log/log.h:561
void log_set_terminal_output(bool enabled)
Control stderr output to terminal.
Definition log/log.c:700
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
#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_MS_INT
Definition time.h:160
asciichat_error_t packet_receive(socket_t sockfd, packet_type_t *type, void **data, size_t *len)
Receive a packet with proper header validation and CRC32 checking.
Definition packet.c:352
asciichat_error_t av_send_audio_opus_batch(socket_t sockfd, const uint8_t *opus_data, size_t opus_size, const uint16_t *frame_sizes, int sample_rate, int frame_duration, int frame_count, crypto_context_t *crypto_ctx)
Send Opus-encoded audio batch packet with encryption support.
Definition packet.c:1175
asciichat_error_t packet_send(socket_t sockfd, packet_type_t type, const void *data, size_t len)
Send a packet with proper header and CRC32.
Definition packet.c:292
#define OPT_PORT_INT_DEFAULT
Default TCP port for client/server communication (integer)
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_CLIENT_LEAVE
Clean disconnect notification.
Definition packet.h:389
@ PACKET_TYPE_IMAGE_FRAME
Complete RGB image with dimensions.
Definition packet.h:363
@ PACKET_TYPE_PONG
Keepalive pong response.
Definition packet.h:385
@ PACKET_TYPE_STREAM_START
Client requests to start sending video/audio.
Definition packet.h:391
@ PACKET_TYPE_ASCII_FRAME
Complete ASCII frame with all metadata.
Definition packet.h:361
@ PACKET_TYPE_CLIENT_CAPABILITIES
Client reports terminal capabilities.
Definition packet.h:381
@ PACKET_TYPE_PING
Keepalive ping packet.
Definition packet.h:383
@ PACKET_TYPE_STREAM_STOP
Client stops sending media.
Definition packet.h:393
#define PALETTE_CHARS_STANDARD
Standard ASCII palette character string.
Definition palette.h:161
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
#define EINTR
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
#define INVALID_SOCKET_VALUE
Invalid socket value (POSIX: -1)
Definition socket.h:278
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
int socket_close(socket_t sock)
Close a socket.
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
int errno
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
#define PIXEL_FORMAT_BGRA
BGRA pixel format.
#define PIXEL_FORMAT_RGB
Packed RGB24 pixel format used by ACIP image-frame senders.
#define PIXEL_FORMAT_BGR
BGR pixel format.
#define PIXEL_FORMAT_RGBA
Packed RGBA32 pixel format.
ringbuffer_t * ringbuffer_create(size_t element_size, size_t capacity)
Create a new ring buffer.
Definition ringbuffer.c:29
void ringbuffer_destroy(ringbuffer_t *rb)
Destroy a ring buffer and free its memory.
Definition ringbuffer.c:53
bool ringbuffer_read(ringbuffer_t *rb, void *data)
Try to read an element from the ring buffer (non-blocking)
Definition ringbuffer.c:82
bool ringbuffer_write(ringbuffer_t *rb, const void *data)
Try to write an element to the ring buffer (non-blocking)
Definition ringbuffer.c:60
void session_host_stop_render(session_host_t *host)
Stop media rendering thread.
Definition host.c:1630
asciichat_error_t session_host_inject_audio(session_host_t *host, uint32_t participant_id, const float *samples, size_t count)
Inject audio samples from memory participant.
Definition host.c:1348
bool session_host_client_has_transport(session_host_t *host, uint32_t client_id)
Check if a specific client has an active alternative transport.
Definition host.c:1738
bool session_host_is_running(session_host_t *host)
Check if host is running.
Definition host.c:1146
void session_audio_destroy(session_audio_ctx_t *ctx)
Destroy session audio context and free resources.
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
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
uint32_t session_host_add_client(session_host_t *host, socket_t socket, const char *ip, int port)
Add a client from an accepted socket.
Definition host.c:1157
void session_host_stop(session_host_t *host)
Stop accepting connections and disconnect all clients.
Definition host.c:1086
void session_display_render_frame(session_display_ctx_t *ctx, const char *frame_data)
Render an ASCII frame to the terminal.
acip_transport_t * session_host_get_client_transport(session_host_t *host, uint32_t client_id)
Get the current transport for a specific client in the host.
Definition host.c:1718
asciichat_error_t session_host_send_frame(session_host_t *host, uint32_t client_id, const char *frame)
Send ASCII frame to a specific client.
Definition host.c:1524
asciichat_error_t session_host_set_client_transport(session_host_t *host, uint32_t client_id, acip_transport_t *transport)
Set an alternative transport for a specific client in the host.
Definition host.c:1666
session_audio_ctx_t * session_audio_create(bool is_host)
Create a new session audio context.
int session_host_get_client_count(session_host_t *host)
Get number of connected clients.
Definition host.c:1460
asciichat_error_t session_host_remove_client(session_host_t *host, uint32_t client_id)
Remove a client by ID.
Definition host.c:1391
asciichat_error_t session_host_find_client(session_host_t *host, uint32_t client_id, session_host_client_info_t *info)
Find a client by ID.
Definition host.c:1435
asciichat_error_t session_host_start(session_host_t *host)
Start accepting client connections.
Definition host.c:1026
int session_host_get_client_ids(session_host_t *host, uint32_t *ids, int max_ids)
Get list of all connected client IDs.
Definition host.c:1467
asciichat_error_t session_host_broadcast_frame(session_host_t *host, const char *frame)
Broadcast ASCII frame to all clients.
Definition host.c:1489
void session_host_destroy(session_host_t *host)
Destroy session host and free resources.
Definition host.c:249
session_host_t * session_host_create(const session_host_config_t *config)
Create a new session host.
Definition host.c:193
asciichat_error_t session_host_start_render(session_host_t *host)
Start media rendering thread (video mixing and audio distribution)
Definition host.c:1571
char g_default_luminance_palette[256]
Default luminance palette (256 characters)
char * ascii_convert(image_t *original, const ssize_t width, const ssize_t height, const bool color, const bool _aspect_ratio, const bool stretch, const char *palette_chars, const char luminance_palette[256])
Convert image to ASCII art.
Definition ascii.c:111
void ascii_simd_init(void)
Initialize SIMD subsystem.
char * ascii_create_grid(ascii_frame_source_t *sources, int source_count, int width, int height, size_t *out_size)
Create a grid layout from multiple ASCII frames.
Definition ascii.c:625
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
struct session_host session_host_t
Internal session host structure.
#define SESSION_HOST_DEFAULT_MAX_CLIENTS
Default maximum clients.
Definition host.c:54
๐Ÿ  Server-side session hosting abstraction
int socket_t
ACIP server-side protocol API.
participant_type_t
Participant type for distinguishing network vs memory participants.
โš™๏ธ Unified options parsing system for ascii-chat with builder pattern and lock-free access
๐Ÿงต Cross-platform thread interface for ascii-chat
asciichat_error_t acip_send_server_state(acip_transport_t *transport, const server_state_packet_t *state)
Send server state update to client (server โ†’ client)
asciichat_error_t acip_send_ascii_frame(acip_transport_t *transport, const char *frame_data, size_t frame_size, uint32_t width, uint32_t height, const char *client_id)
Send ASCII frame to client (server โ†’ client)
#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
Opus audio codec wrapper for real-time encoding/decoding.
โœ… Safe Integer Arithmetic and Overflow Detection
ASCII Palette Management for Video-to-ASCII Conversion.
Cross-platform mutex interface for ascii-chat.
Cross-platform network headers and socket definitions.
Protocol version, features, compression, and frame flag constants.
void audio_recording_submit(audio_recording_source_t source, const float *samples, size_t count, uint64_t start_ns)
Definition recording.c:61
@ AUDIO_RECORDING_REMOTE
Definition recording.h:12
Lock-Free Ring Buffer and Frame Buffer Management.
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.
Cross-platform socket interface for ascii-chat.
int frame
Definition splash.c:99
uint8_t participant_id[16]
๐Ÿ”Š Session-level audio coordination wrapper
๐Ÿ–ฅ๏ธ Unified terminal display abstraction for session-based rendering
Transport instance structure.
Definition transport.h:214
Frame source structure for grid layout.
Definition ascii.h:325
size_t frame_size
Frame data size in bytes (length of frame string)
Definition ascii.h:327
const char * frame_data
Frame data pointer (ASCII frame string, not owned)
Definition ascii.h:326
Image frame packet structure (Packet Type 3)
Definition packet.h:876
uint32_t pixel_format
Pixel format enum (1=RGB24, 2=RGBA32, 3=BGR24, 4=BGRA32)
Definition packet.h:882
uint32_t height
Image height in pixels.
Definition packet.h:880
uint32_t width
Image width in pixels.
Definition packet.h:878
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)
Mutex type (POSIX: pthread_mutex_t with debug tracking)
Opus codec context for encoding or decoding.
Definition opus.h:95
Network packet header structure.
Definition packet.h:598
uint32_t length
Payload data length in bytes (0 for header-only packets)
Definition packet.h:604
uint16_t type
Packet type (packet_type_t enumeration)
Definition packet.h:602
RGB pixel structure.
Lock-free ring buffer structure.
Definition ringbuffer.h:88
Server state packet structure.
Definition packet.h:706
uint32_t active_client_count
Number of clients actively sending video/audio streams.
Definition packet.h:710
uint32_t connected_client_count
Total number of currently connected clients.
Definition packet.h:708
Internal session audio context structure.
Internal session display context structure.
Callback function prototypes for session host events.
Definition host.h:104
void(* on_error)(session_host_t *h, asciichat_error_t error, const char *message, void *user_data)
Called when an error occurs.
Definition host.h:147
void(* on_client_leave)(session_host_t *h, uint32_t client_id, void *user_data)
Called when a client leaves the session.
Definition host.h:119
void(* on_client_join)(session_host_t *h, uint32_t client_id, void *user_data)
Called when a client joins the session.
Definition host.h:111
Client information structure.
Definition host.h:80
uint64_t connected_at
Connection timestamp (Unix time)
Definition host.h:97
int port
Client port.
Definition host.h:88
char ip_address[64]
Client IP address.
Definition host.h:85
bool audio_active
Client is currently streaming audio.
Definition host.h:94
uint32_t client_id
Unique client identifier.
Definition host.h:82
bool video_active
Client is currently streaming video.
Definition host.h:91
Internal client record structure.
Definition host.c:63
ringbuffer_t * incoming_audio
Incoming audio ringbuffer (written by receive loop, read by render thread)
Definition host.c:83
char ip_address[64]
Definition host.c:67
socket_t socket
Definition host.c:66
uint16_t render_height
Definition host.c:74
uint32_t client_id
Definition host.c:65
participant_type_t participant_type
Definition host.c:64
uint64_t connected_at
Definition host.c:72
image_t * incoming_video
Incoming video frame buffer (for host render thread)
Definition host.c:80
uint16_t render_width
Definition host.c:73
struct acip_transport * transport
Alternative transport (WebRTC, WebSocket, etc.) - NULL if using socket only.
Definition host.c:77
Configuration for session host.
Definition host.h:154
void * user_data
User-provided context pointer passed to callbacks.
Definition host.h:180
const char * ipv6_address
IPv6 address to bind to (NULL for any)
Definition host.h:162
session_host_callbacks_t callbacks
Event callbacks.
Definition host.h:177
bool encryption_enabled
Enable encryption (default: true)
Definition host.h:168
const char * key_path
Path to server identity key.
Definition host.h:171
int port
Port to listen on (default: 27224)
Definition host.h:156
const char * password
Password for client authentication (optional)
Definition host.h:174
const char * ipv4_address
IPv4 address to bind to (NULL for any)
Definition host.h:159
int max_clients
Maximum number of clients.
Definition host.h:165
Internal session host structure.
Definition host.c:106
bool running
Server is running.
Definition host.c:144
char ipv6_address[64]
IPv6 bind address.
Definition host.c:114
char password[256]
Password.
Definition host.c:126
char ipv4_address[64]
IPv4 bind address.
Definition host.c:111
mutex_t clients_mutex
Client list mutex.
Definition host.c:156
bool initialized
Context is initialized.
Definition host.c:186
opus_codec_t * opus_encoder
Opus encoder for encoding mixed audio for broadcast.
Definition host.c:183
asciichat_thread_t receive_thread
Receive thread handle.
Definition host.c:165
struct session_display_ctx * display_context
Display context for local frame rendering (optional, can be NULL)
Definition host.c:135
asciichat_thread_t render_thread
Render thread handle (for video mixing and audio distribution)
Definition host.c:171
int client_count
Current client count.
Definition host.c:150
socket_t socket_v6
IPv6 listen socket.
Definition host.c:141
session_host_client_t * clients
Client array.
Definition host.c:147
opus_codec_t * opus_decoder
Opus decoder for decoding incoming Opus audio.
Definition host.c:180
socket_t socket_v4
IPv4 listen socket.
Definition host.c:138
void * user_data
User data for callbacks.
Definition host.c:132
bool receive_thread_running
Receive thread is running.
Definition host.c:168
char key_path[512]
Key path.
Definition host.c:123
session_host_callbacks_t callbacks
Event callbacks.
Definition host.c:129
session_audio_ctx_t * audio_ctx
Audio context for mixing (host only)
Definition host.c:177
asciichat_thread_t accept_thread
Accept thread handle.
Definition host.c:159
int max_clients
Maximum clients.
Definition host.c:117
int port
Port to listen on.
Definition host.c:108
bool render_thread_running
Render thread is running.
Definition host.c:174
uint32_t next_client_id
Next client ID counter.
Definition host.c:153
bool encryption_enabled
Encryption enabled.
Definition host.c:120
bool accept_thread_running
Accept thread is running.
Definition host.c:162
Terminal capabilities packet structure (Packet Type 5)
Definition packet.h:1018
uint16_t height
Terminal height in characters.
Definition packet.h:1030
uint16_t width
Terminal width in characters.
Definition packet.h:1028
โฑ๏ธ High-precision timing utilities using sokol_time.h and uthash
Transport abstraction layer for ACIP protocol.
SIMD-optimized ASCII conversion interface.