ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
host.c
Go to the documentation of this file.
1
16#include <ascii-chat/session/host.h>
17#include <ascii-chat/common.h>
18#include <ascii-chat/options/options.h>
19#include <ascii-chat/asciichat_errno.h>
20#include <ascii-chat/platform/socket.h>
21#include <ascii-chat/platform/mutex.h>
22#include <ascii-chat/platform/thread.h>
23#include <ascii-chat/platform/network.h>
24#include <ascii-chat/log/logging.h>
25#include <ascii-chat/network/client.h>
26#include <ascii-chat/network/packet.h>
27#include <ascii-chat/ringbuffer.h>
28#include <ascii-chat/session/audio.h>
29#include <ascii-chat/audio/opus_codec.h>
30#include <ascii-chat/util/time.h>
31#include <ascii-chat/video/ascii.h>
32
33#include <string.h>
34#include <time.h>
35#include <stdio.h>
36#include <errno.h>
37
38/* ============================================================================
39 * Session Host Constants
40 * ============================================================================ */
41
43#define SESSION_HOST_DEFAULT_MAX_CLIENTS 32
44
45/* ============================================================================
46 * Session Host Internal Types
47 * ============================================================================ */
48
52typedef struct {
53 participant_type_t participant_type; // NETWORK or MEMORY
54 uint32_t client_id;
56 char ip_address[64];
57 int port;
58 bool active;
61 uint64_t connected_at;
62
64 struct acip_transport *transport;
65
68
70 ringbuffer_t *incoming_audio;
72
73/* ============================================================================
74 * Session Host Context Structure
75 * ============================================================================ */
76
82typedef struct session_host {
84 int port;
85
87 char ipv4_address[64];
88
90 char ipv6_address[64];
91
94
97
99 char key_path[512];
100
102 char password[256];
103
105 session_host_callbacks_t callbacks;
106
109
112
115
118
121
124
127
130
132 asciichat_thread_t accept_thread;
133
136
138 asciichat_thread_t receive_thread;
139
142
144 asciichat_thread_t render_thread;
145
148
150 session_audio_ctx_t *audio_ctx;
151
153 opus_codec_t *opus_decoder;
154
156 opus_codec_t *opus_encoder;
157
161
162/* ============================================================================
163 * Session Host Lifecycle Functions
164 * ============================================================================ */
165
166session_host_t *session_host_create(const session_host_config_t *config) {
167 if (!config) {
168 SET_ERRNO(ERROR_INVALID_PARAM, "session_host_create: NULL config");
169 return NULL;
170 }
171
172 // Allocate host
173 session_host_t *host = SAFE_CALLOC(1, sizeof(session_host_t), session_host_t *);
174
175 // Copy configuration
176 host->port = config->port > 0 ? config->port : OPT_PORT_INT_DEFAULT;
177 host->max_clients = config->max_clients > 0 ? config->max_clients : SESSION_HOST_DEFAULT_MAX_CLIENTS;
178 host->encryption_enabled = config->encryption_enabled;
179
180 if (config->ipv4_address) {
181 SAFE_STRNCPY(host->ipv4_address, config->ipv4_address, sizeof(host->ipv4_address));
182 }
183
184 if (config->ipv6_address) {
185 SAFE_STRNCPY(host->ipv6_address, config->ipv6_address, sizeof(host->ipv6_address));
186 }
187
188 if (config->key_path) {
189 SAFE_STRNCPY(host->key_path, config->key_path, sizeof(host->key_path));
190 }
191
192 if (config->password) {
193 SAFE_STRNCPY(host->password, config->password, sizeof(host->password));
194 }
195
196 host->callbacks = config->callbacks;
197 host->user_data = config->user_data;
198
199 // Initialize sockets to invalid
200 host->socket_v4 = INVALID_SOCKET_VALUE;
201 host->socket_v6 = INVALID_SOCKET_VALUE;
202 host->running = false;
203
204 // Allocate client array
205 host->clients = SAFE_CALLOC((size_t)host->max_clients, sizeof(session_host_client_t), session_host_client_t *);
206
207 host->client_count = 0;
208 host->next_client_id = 1;
209
210 // Initialize mutex
211 if (mutex_init(&host->clients_mutex) != 0) {
212 SET_ERRNO(ERROR_THREAD, "Failed to initialize clients mutex");
213 SAFE_FREE(host->clients);
214 SAFE_FREE(host);
215 return NULL;
216 }
217
218 host->initialized = true;
219 return host;
220}
221
223 if (!host) {
224 return;
225 }
226
227 // Stop if running
228 if (host->running) {
229 session_host_stop(host);
230 }
231
232 // Clean up audio resources
233 if (host->audio_ctx) {
235 host->audio_ctx = NULL;
236 }
237 if (host->opus_decoder) {
239 host->opus_decoder = NULL;
240 }
241
242 // Close sockets
243 if (host->socket_v4 != INVALID_SOCKET_VALUE) {
244 socket_close(host->socket_v4);
245 host->socket_v4 = INVALID_SOCKET_VALUE;
246 }
247 if (host->socket_v6 != INVALID_SOCKET_VALUE) {
248 socket_close(host->socket_v6);
249 host->socket_v6 = INVALID_SOCKET_VALUE;
250 }
251
252 // Free client array and per-client resources
253 if (host->clients) {
254 for (int i = 0; i < host->max_clients; i++) {
255 if (host->clients[i].incoming_video) {
257 host->clients[i].incoming_video = NULL;
258 }
259 if (host->clients[i].incoming_audio) {
261 host->clients[i].incoming_audio = NULL;
262 }
263 }
264 SAFE_FREE(host->clients);
265 }
266
267 // Destroy mutex
269
270 // Clear sensitive data
271 memset(host->password, 0, sizeof(host->password));
272
273 host->initialized = false;
274 SAFE_FREE(host);
275}
276
277/* ============================================================================
278 * Session Host Server Control Functions
279 * ============================================================================ */
280
285static socket_t create_listen_socket(const char *address, int port) {
286 struct addrinfo hints, *result = NULL, *rp = NULL;
287 char port_str[16];
288
289 if (!address)
290 address = "0.0.0.0";
291
292 memset(&hints, 0, sizeof(hints));
293 hints.ai_family = AF_INET; // IPv4
294 hints.ai_socktype = SOCK_STREAM;
295 hints.ai_flags = AI_PASSIVE;
296
297 safe_snprintf(port_str, sizeof(port_str), "%d", port);
298
299 int s = getaddrinfo(address, port_str, &hints, &result);
300 if (s != 0) {
301 SET_ERRNO(ERROR_NETWORK, "getaddrinfo failed: %s", gai_strerror(s));
302 return INVALID_SOCKET_VALUE;
303 }
304
305 socket_t listen_sock = INVALID_SOCKET_VALUE;
306 for (rp = result; rp != NULL; rp = rp->ai_next) {
307 listen_sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
308 if (listen_sock == INVALID_SOCKET_VALUE) {
309 continue;
310 }
311
312 // Set SO_REUSEADDR to allow rebinding quickly after restart
313 int reuse = 1;
314 if (setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(reuse)) < 0) {
315 log_warn("setsockopt SO_REUSEADDR failed");
316 }
317
318 if (bind(listen_sock, rp->ai_addr, (int)rp->ai_addrlen) == 0) {
319 break; // Success
320 }
321
322 socket_close(listen_sock);
323 listen_sock = INVALID_SOCKET_VALUE;
324 }
325
326 freeaddrinfo(result);
327
328 if (listen_sock == INVALID_SOCKET_VALUE) {
329 SET_ERRNO_SYS(ERROR_NETWORK_BIND, "Failed to bind listen socket on %s:%d", address, port);
330 return INVALID_SOCKET_VALUE;
331 }
332
333 if (listen(listen_sock, SOMAXCONN) != 0) {
334 SET_ERRNO_SYS(ERROR_NETWORK_BIND, "listen() failed on %s:%d", address, port);
335 socket_close(listen_sock);
336 return INVALID_SOCKET_VALUE;
337 }
338
339 return listen_sock;
340}
341
350static void *accept_loop_thread(void *arg) {
351 session_host_t *host = (session_host_t *)arg;
352 if (!host)
353 return NULL;
354
355 log_info("Accept loop started");
356
357 // Use select() to handle accept with timeout
358 struct timeval tv;
359 fd_set readfds;
360 int max_fd;
361
362 while (host->accept_thread_running && host->running) {
363 // Set timeout to 1 second
364 tv.tv_sec = 1;
365 tv.tv_usec = 0;
366
367 FD_ZERO(&readfds);
368 max_fd = 0;
369
370 if (host->socket_v4 != INVALID_SOCKET_VALUE) {
371 FD_SET(host->socket_v4, &readfds);
372 max_fd = (int)host->socket_v4;
373 }
374
375 // Wait for incoming connections
376 int activity = select(max_fd + 1, &readfds, NULL, NULL, &tv);
377 if (activity < 0) {
378 if (errno != EINTR) {
379 log_error("select() failed");
380 }
381 continue;
382 }
383
384 if (activity == 0) {
385 // Timeout - check if we should exit
386 continue;
387 }
388
389 // Check for incoming connections
390 if (host->socket_v4 != INVALID_SOCKET_VALUE && FD_ISSET(host->socket_v4, &readfds)) {
391 struct sockaddr_in client_addr;
392 socklen_t client_addr_len = sizeof(client_addr);
393
394 // Accept incoming connection
395 socket_t client_socket = accept(host->socket_v4, (struct sockaddr *)&client_addr, &client_addr_len);
396 if (client_socket == INVALID_SOCKET_VALUE) {
397 log_warn("accept() failed");
398 continue;
399 }
400
401 // Get client IP and port
402 char client_ip[64];
403 inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
404 int client_port = ntohs(client_addr.sin_port);
405
406 log_info("New connection from %s:%d", client_ip, client_port);
407
408 // Add client to host
409 uint32_t client_id = session_host_add_client(host, client_socket, client_ip, client_port);
410 if (client_id == 0) {
411 log_error("Failed to add client");
412 socket_close(client_socket);
413 }
414 }
415 }
416
417 log_info("Accept loop stopped");
418 return NULL;
419}
420
429static void *receive_loop_thread(void *arg) {
430 session_host_t *host = (session_host_t *)arg;
431 if (!host)
432 return NULL;
433
434 log_info("Receive loop started");
435
436 struct timeval tv;
437 fd_set readfds;
438 socket_t max_fd;
439
440 while (host->receive_thread_running && host->running) {
441 // Set timeout to 1 second
442 tv.tv_sec = 1;
443 tv.tv_usec = 0;
444
445 FD_ZERO(&readfds);
446 max_fd = INVALID_SOCKET_VALUE;
447
448 // Add all active client sockets to the set
449 mutex_lock(&host->clients_mutex);
450 for (int i = 0; i < host->max_clients; i++) {
451 if (host->clients[i].active && host->clients[i].socket != INVALID_SOCKET_VALUE) {
452 FD_SET(host->clients[i].socket, &readfds);
453 if (max_fd == INVALID_SOCKET_VALUE || host->clients[i].socket > max_fd) {
454 max_fd = host->clients[i].socket;
455 }
456 }
457 }
458 mutex_unlock(&host->clients_mutex);
459
460 // If no clients, just wait for timeout
461 if (max_fd == INVALID_SOCKET_VALUE) {
462 continue;
463 }
464
465 // Wait for data on any client socket
466 int activity = select((int)max_fd + 1, &readfds, NULL, NULL, &tv);
467 if (activity < 0) {
468 if (errno != EINTR) {
469 log_error("select() failed in receive loop");
470 }
471 continue;
472 }
473
474 if (activity == 0) {
475 // Timeout - check if we should exit
476 continue;
477 }
478
479 // Check each client for incoming data
480 mutex_lock(&host->clients_mutex);
481 for (int i = 0; i < host->max_clients; i++) {
482 if (!host->clients[i].active || host->clients[i].socket == INVALID_SOCKET_VALUE) {
483 continue;
484 }
485
486 if (!FD_ISSET(host->clients[i].socket, &readfds)) {
487 continue;
488 }
489
490 // Try to receive packet from this client
491 packet_type_t ptype;
492 void *data = NULL;
493 size_t len = 0;
494 asciichat_error_t result = packet_receive(host->clients[i].socket, &ptype, &data, &len);
495
496 if (result != ASCIICHAT_OK) {
497 log_warn("packet_receive failed from client %u: %d", host->clients[i].client_id, result);
498 // Client disconnected or error - will be cleaned up by timeout mechanism
499 continue;
500 }
501
502 // Process packet based on type
503 uint32_t client_id = host->clients[i].client_id;
504 socket_t client_socket = host->clients[i].socket;
505 mutex_unlock(&host->clients_mutex);
506
507 switch (ptype) {
508 case PACKET_TYPE_IMAGE_FRAME:
509 // Client sent a video frame - parse and store in incoming buffer
510 if (data && len >= sizeof(image_frame_packet_t)) {
511 const image_frame_packet_t *frame_hdr = (const image_frame_packet_t *)data;
512 const uint8_t *pixel_data = (const uint8_t *)data + sizeof(image_frame_packet_t);
513 size_t pixel_data_size = len - sizeof(image_frame_packet_t);
514
515 // Find client and store frame in incoming_video buffer
516 mutex_lock(&host->clients_mutex);
517 for (int j = 0; j < host->max_clients; j++) {
518 if (host->clients[j].client_id == client_id && host->clients[j].incoming_video) {
519 // Store frame in the image buffer
520 image_t *img = host->clients[j].incoming_video;
521 if (img->w == (int)frame_hdr->width && img->h == (int)frame_hdr->height) {
522 // Copy pixel data (RGB format)
523 size_t expected_size = (size_t)frame_hdr->width * frame_hdr->height * 3;
524 if (pixel_data_size >= expected_size) {
525 memcpy(img->pixels, pixel_data, expected_size);
526 log_debug_every(500 * US_PER_MS_INT, "Frame received from client %u (%ux%u)", client_id,
527 frame_hdr->width, frame_hdr->height);
528 }
529 }
530 break;
531 }
532 }
533 mutex_unlock(&host->clients_mutex);
534 }
535 break;
536
537 case PACKET_TYPE_AUDIO_OPUS_BATCH:
538 // Client sent Opus-encoded audio batch
539 if (data && len > 16) { // Must have at least header
540 const uint8_t *batch_data = (const uint8_t *)data;
541 // Parse header: sample_rate (4), frame_duration (4), frame_count (4), reserved (4)
542 (void)batch_data[0]; // Avoid unused variable warning if we don't use sample_rate/frame_duration
543 uint32_t batch_frame_count = *(const uint32_t *)(batch_data + 8);
544
545 if (batch_frame_count > 0 && batch_frame_count <= 1000) {
546 const uint16_t *frame_sizes = (const uint16_t *)(batch_data + 16);
547 const uint8_t *opus_frames = batch_data + 16 + (batch_frame_count * sizeof(uint16_t));
548
549 // Find client and decode audio
550 mutex_lock(&host->clients_mutex);
551 for (int j = 0; j < host->max_clients; j++) {
552 if (host->clients[j].client_id == client_id && host->clients[j].incoming_audio && host->opus_decoder) {
553 // Decode each Opus frame and write to ringbuffer
554 const uint8_t *current_frame = opus_frames;
555 for (uint32_t k = 0; k < batch_frame_count; k++) {
556 uint16_t frame_size = frame_sizes[k];
557 if (frame_size > 0) {
558 // Allocate buffer for decoded samples
559 float decoded_samples[960]; // Max 20ms @ 48kHz
560 int decoded_count =
561 opus_codec_decode(host->opus_decoder, current_frame, (int)frame_size, decoded_samples, 960);
562 if (decoded_count > 0) {
563 // Write samples to ringbuffer one at a time
564 for (int s = 0; s < decoded_count; s++) {
565 ringbuffer_write(host->clients[j].incoming_audio, &decoded_samples[s]);
566 }
567 }
568 }
569 current_frame += frame_size;
570 }
571 log_debug_every(NS_PER_MS_INT, "Audio batch received from client %u (%u frames)", client_id,
572 batch_frame_count);
573 break;
574 }
575 }
576 mutex_unlock(&host->clients_mutex);
577 }
578 }
579 break;
580
581 case PACKET_TYPE_STREAM_START:
582 log_info("Client %u started streaming", client_id);
583 mutex_lock(&host->clients_mutex);
584 for (int j = 0; j < host->max_clients; j++) {
585 if (host->clients[j].client_id == client_id) {
586 host->clients[j].video_active = true;
587 break;
588 }
589 }
590 mutex_unlock(&host->clients_mutex);
591 break;
592
593 case PACKET_TYPE_STREAM_STOP:
594 log_info("Client %u stopped streaming", client_id);
595 mutex_lock(&host->clients_mutex);
596 for (int j = 0; j < host->max_clients; j++) {
597 if (host->clients[j].client_id == client_id) {
598 host->clients[j].video_active = false;
599 break;
600 }
601 }
602 mutex_unlock(&host->clients_mutex);
603 break;
604
605 case PACKET_TYPE_PING:
606 // Respond with PONG
607 log_debug_every(NS_PER_MS_INT, "PING from client %u", client_id);
608 packet_send(client_socket, PACKET_TYPE_PONG, NULL, 0);
609 break;
610
611 case PACKET_TYPE_CLIENT_LEAVE:
612 log_info("Client %u requested disconnect", client_id);
613 // Mark for removal (will be handled separately to avoid deadlock)
614 break;
615
616 default:
617 log_warn("Unknown packet type %u from client %u", ptype, client_id);
618 break;
619 }
620
621 // Free packet data
622 if (data) {
623 SAFE_FREE(data);
624 }
625
626 mutex_lock(&host->clients_mutex);
627 }
628 mutex_unlock(&host->clients_mutex);
629 }
630
631 log_info("Receive loop stopped");
632 return NULL;
633}
634
644static void *host_render_thread(void *arg) {
645 session_host_t *host = (session_host_t *)arg;
646 if (!host) {
647 SET_ERRNO(ERROR_INVALID_PARAM, "host_render_thread: invalid host");
648 return NULL;
649 }
650
651 log_info("Host render thread started");
652
653 uint64_t last_video_render_ns = 0;
654 uint64_t last_audio_render_ns = 0;
655
656 while (host->render_thread_running && host->running) {
657 uint64_t now_ns = time_get_ns();
658
659 // VIDEO RENDERING (60 FPS = 16.7ms)
660 if (time_elapsed_ns(last_video_render_ns, now_ns) >= NS_PER_MS_INT * 16) {
661 // Collect video frames from all participants and create grid layout
662 mutex_lock(&host->clients_mutex);
663
664 // Count active participants with video
665 int active_video_count = 0;
666 for (int i = 0; i < host->max_clients; i++) {
667 if (host->clients[i].active && host->clients[i].video_active && host->clients[i].incoming_video) {
668 active_video_count++;
669 }
670 }
671
672 // If we have active participants, generate and broadcast grid
673 if (active_video_count > 0) {
674 // Allocate arrays for ASCII frames and sources
675 char **ascii_frames = SAFE_MALLOC(active_video_count * sizeof(char *), char **);
676 ascii_frame_source_t *sources =
677 SAFE_MALLOC(active_video_count * sizeof(ascii_frame_source_t), ascii_frame_source_t *);
678
679 if (ascii_frames && sources) {
680 // Convert each incoming video frame to ASCII
681 int frame_idx = 0;
682 for (int i = 0; i < host->max_clients; i++) {
683 if (host->clients[i].active && host->clients[i].video_active && host->clients[i].incoming_video) {
684 image_t *img = host->clients[i].incoming_video;
685
686 // Convert image to ASCII (80x24 for each frame in grid, monochrome for now)
687 extern char g_default_luminance_palette[256];
688 ascii_frames[frame_idx] =
689 ascii_convert(img, 80, 24, false, false, false, NULL, g_default_luminance_palette);
690 if (ascii_frames[frame_idx]) {
691 sources[frame_idx].frame_data = ascii_frames[frame_idx];
692 sources[frame_idx].frame_size = strlen(ascii_frames[frame_idx]) + 1;
693 } else {
694 sources[frame_idx].frame_data = "";
695 sources[frame_idx].frame_size = 1;
696 }
697 frame_idx++;
698 }
699 }
700
701 // Create grid layout from all ASCII frames
702 size_t grid_size = 0;
703 char *grid_frame = ascii_create_grid(sources, active_video_count, 80, 24, &grid_size);
704
705 if (grid_frame) {
706 // Broadcast grid to all participants
707 for (int i = 0; i < host->max_clients; i++) {
708 if (host->clients[i].active && host->clients[i].socket != INVALID_SOCKET_VALUE) {
709 packet_send(host->clients[i].socket, PACKET_TYPE_ASCII_FRAME, grid_frame, grid_size);
710 }
711 }
712 SAFE_FREE(grid_frame);
713 }
714
715 // Free ASCII frames
716 for (int i = 0; i < active_video_count; i++) {
717 if (ascii_frames[i]) {
718 SAFE_FREE(ascii_frames[i]);
719 }
720 }
721 }
722
723 SAFE_FREE(ascii_frames);
724 SAFE_FREE(sources);
725 }
726
727 mutex_unlock(&host->clients_mutex);
728 log_debug_every(NS_PER_MS_INT, "Video render cycle (%d active)", active_video_count);
729 last_video_render_ns = now_ns;
730 }
731
732 // AUDIO RENDERING (100 FPS = 10ms)
733 if (time_elapsed_ns(last_audio_render_ns, now_ns) >= NS_PER_MS_INT * 10) {
734 // Mix audio from all participants
735 // 1. Read samples from each participant's incoming_audio ringbuffer
736 // 2. Mix into output buffer (simple addition with clipping)
737 // 3. Encode with Opus
738 // 4. Broadcast mixed audio via av_send_audio_opus_batch()
739
740 if (host->audio_ctx && host->opus_encoder) {
741 float mixed_audio[960]; // 20ms @ 48kHz
742 memset(mixed_audio, 0, sizeof(mixed_audio));
743
744 // Lock clients mutex to safely read audio buffers
745 mutex_lock(&host->clients_mutex);
746 for (int i = 0; i < host->max_clients; i++) {
747 if (!host->clients[i].active || !host->clients[i].audio_active) {
748 continue;
749 }
750
751 if (!host->clients[i].incoming_audio) {
752 continue;
753 }
754
755 // Read audio samples one-by-one from this participant's ringbuffer
756 // ringbuffer_read() reads one element (1 float) per call
757 for (int j = 0; j < 960; j++) {
758 float sample = 0.0f;
759 if (ringbuffer_read(host->clients[i].incoming_audio, &sample)) {
760 // Successfully read a sample
761 mixed_audio[j] += sample;
762 // Clip to [-1.0, 1.0] to prevent distortion
763 if (mixed_audio[j] > 1.0f) {
764 mixed_audio[j] = 1.0f;
765 } else if (mixed_audio[j] < -1.0f) {
766 mixed_audio[j] = -1.0f;
767 }
768 }
769 // If ringbuffer is empty, sample remains 0.0f and that's fine
770 }
771 }
772 mutex_unlock(&host->clients_mutex);
773
774 // Encode to Opus
775 uint8_t opus_buffer[1000];
776 size_t opus_len = opus_codec_encode(host->opus_encoder, mixed_audio, 960, opus_buffer, sizeof(opus_buffer));
777
778 if (opus_len > 0) {
779 // Broadcast mixed audio to all participants
780 uint16_t frame_sizes[1] = {(uint16_t)opus_len};
781 av_send_audio_opus_batch(host->socket_v4, opus_buffer, opus_len, frame_sizes, 48000, 20, 1, NULL);
782 }
783 }
784
785 log_debug_every(NS_PER_MS_INT, "Audio render cycle");
786 last_audio_render_ns = now_ns;
787 }
788
789 // Small sleep to prevent busy-loop
791 }
792
793 log_info("Host render thread stopped");
794 return NULL;
795}
796
797asciichat_error_t session_host_start(session_host_t *host) {
798 if (!host || !host->initialized) {
799 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_start: invalid host");
800 }
801
802 if (host->running) {
803 return ASCIICHAT_OK; // Already running
804 }
805
806 // Create listen socket(s)
807 // For simplicity, we bind to IPv4 if specified, otherwise default to 0.0.0.0
808 const char *bind_address = host->ipv4_address[0] ? host->ipv4_address : "0.0.0.0";
809
810 host->socket_v4 = create_listen_socket(bind_address, host->port);
811 if (host->socket_v4 == INVALID_SOCKET_VALUE) {
812 log_error("Failed to create IPv4 listen socket");
813 if (host->callbacks.on_error) {
814 host->callbacks.on_error(host, ERROR_NETWORK_BIND, "Failed to create listen socket", host->user_data);
815 }
816 return GET_ERRNO();
817 }
818
819 host->running = true;
820 log_info("Session host listening on %s:%d", bind_address, host->port);
821
822 // Spawn accept loop thread
823 host->accept_thread_running = true;
824 if (asciichat_thread_create(&host->accept_thread, accept_loop_thread, host) != 0) {
825 log_error("Failed to spawn accept loop thread");
826 host->accept_thread_running = false;
827 if (host->callbacks.on_error) {
828 host->callbacks.on_error(host, ERROR_THREAD, "Failed to spawn accept loop thread", host->user_data);
829 }
830 socket_close(host->socket_v4);
831 host->socket_v4 = INVALID_SOCKET_VALUE;
832 host->running = false;
833 return SET_ERRNO(ERROR_THREAD, "Failed to spawn accept loop thread");
834 }
835
836 // Spawn receive loop thread
837 host->receive_thread_running = true;
838 if (asciichat_thread_create(&host->receive_thread, receive_loop_thread, host) != 0) {
839 log_error("Failed to spawn receive loop thread");
840 host->receive_thread_running = false;
841 host->accept_thread_running = false;
843 if (host->callbacks.on_error) {
844 host->callbacks.on_error(host, ERROR_THREAD, "Failed to spawn receive loop thread", host->user_data);
845 }
846 socket_close(host->socket_v4);
847 host->socket_v4 = INVALID_SOCKET_VALUE;
848 host->running = false;
849 return SET_ERRNO(ERROR_THREAD, "Failed to spawn receive loop thread");
850 }
851
852 // Spawn render thread (optional - can be started later)
853 // This thread handles video mixing and audio distribution
854 return ASCIICHAT_OK;
855}
856
858 if (!host || !host->initialized || !host->running) {
859 return;
860 }
861
862 // Stop render thread if running
863 if (host->render_thread_running) {
864 host->render_thread_running = false;
866 log_info("Render thread joined");
867 }
868
869 // Stop receive loop thread (reads from client sockets)
870 if (host->receive_thread_running) {
871 host->receive_thread_running = false;
873 log_info("Receive loop thread joined");
874 }
875
876 // Stop accept loop thread (before closing listen socket)
877 if (host->accept_thread_running) {
878 host->accept_thread_running = false;
880 log_info("Accept loop thread joined");
881 }
882
883 // Disconnect all clients
884 mutex_lock(&host->clients_mutex);
885 for (int i = 0; i < host->max_clients; i++) {
886 if (host->clients[i].active) {
887 // Invoke callback
888 if (host->callbacks.on_client_leave) {
889 host->callbacks.on_client_leave(host, host->clients[i].client_id, host->user_data);
890 }
891
892 // Close socket
893 if (host->clients[i].socket != INVALID_SOCKET_VALUE) {
894 socket_close(host->clients[i].socket);
895 host->clients[i].socket = INVALID_SOCKET_VALUE;
896 }
897
898 host->clients[i].active = false;
899 }
900 }
901 host->client_count = 0;
902 mutex_unlock(&host->clients_mutex);
903
904 // Close listen sockets
905 if (host->socket_v4 != INVALID_SOCKET_VALUE) {
906 socket_close(host->socket_v4);
907 host->socket_v4 = INVALID_SOCKET_VALUE;
908 }
909 if (host->socket_v6 != INVALID_SOCKET_VALUE) {
910 socket_close(host->socket_v6);
911 host->socket_v6 = INVALID_SOCKET_VALUE;
912 }
913
914 host->running = false;
915}
916
918 if (!host || !host->initialized) {
919 return false;
920 }
921 return host->running;
922}
923
924/* ============================================================================
925 * Session Host Client Management Functions
926 * ============================================================================ */
927
928uint32_t session_host_add_client(session_host_t *host, socket_t socket, const char *ip, int port) {
929 if (!host || !host->initialized) {
930 return 0;
931 }
932
933 mutex_lock(&host->clients_mutex);
934
935 // Check if we have room
936 if (host->client_count >= host->max_clients) {
937 mutex_unlock(&host->clients_mutex);
938 SET_ERRNO(ERROR_SESSION_FULL, "Maximum clients reached");
939 return 0;
940 }
941
942 // Find empty slot
943 for (int i = 0; i < host->max_clients; i++) {
944 if (!host->clients[i].active) {
945 host->clients[i].participant_type = PARTICIPANT_TYPE_NETWORK;
946 host->clients[i].client_id = host->next_client_id++;
947 host->clients[i].socket = socket;
948 if (ip) {
949 SAFE_STRNCPY(host->clients[i].ip_address, ip, sizeof(host->clients[i].ip_address));
950 }
951 host->clients[i].port = port;
952 host->clients[i].active = true;
953 host->clients[i].video_active = false;
954 host->clients[i].audio_active = false;
955 host->clients[i].connected_at = (uint64_t)time(NULL);
956 host->clients[i].transport = NULL; // No alternative transport initially
957
958 // Allocate media buffers
959 host->clients[i].incoming_video = image_new(480, 270); // Network-optimal size (HD preview)
960 host->clients[i].incoming_audio = ringbuffer_create(sizeof(float), 960 * 10); // ~200ms buffer @ 48kHz
961
962 if (!host->clients[i].incoming_video || !host->clients[i].incoming_audio) {
963 // Cleanup on allocation failure
964 if (host->clients[i].incoming_video) {
966 host->clients[i].incoming_video = NULL;
967 }
968 if (host->clients[i].incoming_audio) {
970 host->clients[i].incoming_audio = NULL;
971 }
972 host->clients[i].active = false;
973 mutex_unlock(&host->clients_mutex);
974 SET_ERRNO(ERROR_MEMORY, "Failed to allocate media buffers for client");
975 return 0;
976 }
977
978 uint32_t client_id = host->clients[i].client_id;
979 host->client_count++;
980
981 mutex_unlock(&host->clients_mutex);
982
983 // Invoke callback
984 if (host->callbacks.on_client_join) {
985 host->callbacks.on_client_join(host, client_id, host->user_data);
986 }
987
988 return client_id;
989 }
990 }
991
992 mutex_unlock(&host->clients_mutex);
993 return 0;
994}
995
997 if (!host || !host->initialized) {
998 return 0;
999 }
1000
1001 mutex_lock(&host->clients_mutex);
1002
1003 // Check if we have room
1004 if (host->client_count >= host->max_clients) {
1005 mutex_unlock(&host->clients_mutex);
1006 SET_ERRNO(ERROR_SESSION_FULL, "Maximum clients reached");
1007 return 0;
1008 }
1009
1010 // Check if memory participant already exists (only one allowed)
1011 for (int i = 0; i < host->max_clients; i++) {
1012 if (host->clients[i].active && host->clients[i].participant_type == PARTICIPANT_TYPE_MEMORY) {
1013 mutex_unlock(&host->clients_mutex);
1014 SET_ERRNO(ERROR_INVALID_PARAM, "Memory participant already exists");
1015 return 0;
1016 }
1017 }
1018
1019 // Find empty slot
1020 for (int i = 0; i < host->max_clients; i++) {
1021 if (!host->clients[i].active) {
1022 host->clients[i].participant_type = PARTICIPANT_TYPE_MEMORY;
1023 host->clients[i].client_id = host->next_client_id++;
1024 host->clients[i].socket = INVALID_SOCKET_VALUE; // No socket for memory participant
1025 SAFE_STRNCPY(host->clients[i].ip_address, "memory", sizeof(host->clients[i].ip_address));
1026 host->clients[i].port = 0;
1027 host->clients[i].active = true;
1028 host->clients[i].video_active = false;
1029 host->clients[i].audio_active = false;
1030 host->clients[i].connected_at = (uint64_t)time(NULL);
1031 host->clients[i].transport = NULL;
1032
1033 // Allocate media buffers (same as network clients)
1034 host->clients[i].incoming_video = image_new(480, 270);
1035 host->clients[i].incoming_audio = ringbuffer_create(sizeof(float), 960 * 10);
1036
1037 if (!host->clients[i].incoming_video || !host->clients[i].incoming_audio) {
1038 if (host->clients[i].incoming_video) {
1040 host->clients[i].incoming_video = NULL;
1041 }
1042 if (host->clients[i].incoming_audio) {
1044 host->clients[i].incoming_audio = NULL;
1045 }
1046 host->clients[i].active = false;
1047 mutex_unlock(&host->clients_mutex);
1048 SET_ERRNO(ERROR_MEMORY, "Failed to allocate media buffers for memory participant");
1049 return 0;
1050 }
1051
1052 uint32_t participant_id = host->clients[i].client_id;
1053 host->client_count++;
1054
1055 mutex_unlock(&host->clients_mutex);
1056
1057 log_info("Added memory participant with ID %u", participant_id);
1058
1059 // Invoke callback
1060 if (host->callbacks.on_client_join) {
1061 host->callbacks.on_client_join(host, participant_id, host->user_data);
1062 }
1063
1064 return participant_id;
1065 }
1066 }
1067
1068 mutex_unlock(&host->clients_mutex);
1069 return 0;
1070}
1071
1072asciichat_error_t session_host_inject_frame(session_host_t *host, uint32_t participant_id, const image_t *frame) {
1073 if (!host || !host->initialized || !frame) {
1074 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_inject_frame: invalid parameters");
1075 }
1076
1077 mutex_lock(&host->clients_mutex);
1078
1079 // Find memory participant
1080 for (int i = 0; i < host->max_clients; i++) {
1081 if (host->clients[i].active && host->clients[i].client_id == participant_id &&
1082 host->clients[i].participant_type == PARTICIPANT_TYPE_MEMORY) {
1083
1084 if (!host->clients[i].incoming_video) {
1085 mutex_unlock(&host->clients_mutex);
1086 return SET_ERRNO(ERROR_INVALID_STATE, "Memory participant has no video buffer");
1087 }
1088
1089 // Copy frame data into buffer
1090 image_t *dest = host->clients[i].incoming_video;
1091 if (dest->w != frame->w || dest->h != frame->h) {
1092 // Reallocate if size changed
1093 image_destroy(dest);
1094 dest = image_new(frame->w, frame->h);
1095 if (!dest) {
1096 host->clients[i].incoming_video = NULL;
1097 mutex_unlock(&host->clients_mutex);
1098 return SET_ERRNO(ERROR_MEMORY, "Failed to reallocate video buffer");
1099 }
1100 host->clients[i].incoming_video = dest;
1101 }
1102
1103 // Copy pixel data
1104 memcpy(dest->pixels, frame->pixels, frame->w * frame->h * sizeof(rgb_pixel_t));
1105
1106 host->clients[i].video_active = true;
1107
1108 mutex_unlock(&host->clients_mutex);
1109 return ASCIICHAT_OK;
1110 }
1111 }
1112
1113 mutex_unlock(&host->clients_mutex);
1114 return SET_ERRNO(ERROR_NOT_FOUND, "Memory participant not found");
1115}
1116
1117asciichat_error_t session_host_inject_audio(session_host_t *host, uint32_t participant_id, const float *samples,
1118 size_t count) {
1119 if (!host || !host->initialized || !samples || count == 0) {
1120 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_inject_audio: invalid parameters");
1121 }
1122
1123 mutex_lock(&host->clients_mutex);
1124
1125 // Find memory participant
1126 for (int i = 0; i < host->max_clients; i++) {
1127 if (host->clients[i].active && host->clients[i].client_id == participant_id &&
1128 host->clients[i].participant_type == PARTICIPANT_TYPE_MEMORY) {
1129
1130 if (!host->clients[i].incoming_audio) {
1131 mutex_unlock(&host->clients_mutex);
1132 return SET_ERRNO(ERROR_INVALID_STATE, "Memory participant has no audio buffer");
1133 }
1134
1135 // Write samples to ringbuffer (one at a time)
1136 size_t written = 0;
1137 for (size_t j = 0; j < count; j++) {
1138 if (ringbuffer_write(host->clients[i].incoming_audio, &samples[j])) {
1139 written++;
1140 } else {
1141 break; // Buffer full
1142 }
1143 }
1144
1145 if (written < count) {
1146 log_warn_every(NS_PER_MS_INT, "Audio ringbuffer full, dropped %zu samples", count - written);
1147 }
1148
1149 host->clients[i].audio_active = true;
1150
1151 mutex_unlock(&host->clients_mutex);
1152 return ASCIICHAT_OK;
1153 }
1154 }
1155
1156 mutex_unlock(&host->clients_mutex);
1157 return SET_ERRNO(ERROR_NOT_FOUND, "Memory participant not found");
1158}
1159
1160asciichat_error_t session_host_remove_client(session_host_t *host, uint32_t client_id) {
1161 if (!host || !host->initialized) {
1162 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_remove_client: invalid host");
1163 }
1164
1165 mutex_lock(&host->clients_mutex);
1166
1167 for (int i = 0; i < host->max_clients; i++) {
1168 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1169 // Invoke callback before removing
1170 mutex_unlock(&host->clients_mutex);
1171 if (host->callbacks.on_client_leave) {
1172 host->callbacks.on_client_leave(host, client_id, host->user_data);
1173 }
1174 mutex_lock(&host->clients_mutex);
1175
1176 // Close socket
1177 if (host->clients[i].socket != INVALID_SOCKET_VALUE) {
1178 socket_close(host->clients[i].socket);
1179 host->clients[i].socket = INVALID_SOCKET_VALUE;
1180 }
1181
1182 // Clean up media buffers
1183 if (host->clients[i].incoming_video) {
1185 host->clients[i].incoming_video = NULL;
1186 }
1187 if (host->clients[i].incoming_audio) {
1189 host->clients[i].incoming_audio = NULL;
1190 }
1191
1192 host->clients[i].active = false;
1193 host->client_count--;
1194
1195 mutex_unlock(&host->clients_mutex);
1196 return ASCIICHAT_OK;
1197 }
1198 }
1199
1200 mutex_unlock(&host->clients_mutex);
1201 return SET_ERRNO(ERROR_NOT_FOUND, "Client not found: %u", client_id);
1202}
1203
1204asciichat_error_t session_host_find_client(session_host_t *host, uint32_t client_id, session_host_client_info_t *info) {
1205 if (!host || !host->initialized || !info) {
1206 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_find_client: invalid parameter");
1207 }
1208
1209 mutex_lock(&host->clients_mutex);
1210
1211 for (int i = 0; i < host->max_clients; i++) {
1212 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1213 info->client_id = host->clients[i].client_id;
1214 SAFE_STRNCPY(info->ip_address, host->clients[i].ip_address, sizeof(info->ip_address));
1215 info->port = host->clients[i].port;
1216 info->video_active = host->clients[i].video_active;
1217 info->audio_active = host->clients[i].audio_active;
1218 info->connected_at = host->clients[i].connected_at;
1219
1220 mutex_unlock(&host->clients_mutex);
1221 return ASCIICHAT_OK;
1222 }
1223 }
1224
1225 mutex_unlock(&host->clients_mutex);
1226 return SET_ERRNO(ERROR_NOT_FOUND, "Client not found: %u", client_id);
1227}
1228
1230 if (!host || !host->initialized) {
1231 return 0;
1232 }
1233 return host->client_count;
1234}
1235
1236int session_host_get_client_ids(session_host_t *host, uint32_t *ids, int max_ids) {
1237 if (!host || !host->initialized || !ids || max_ids <= 0) {
1238 return 0;
1239 }
1240
1241 mutex_lock(&host->clients_mutex);
1242
1243 int count = 0;
1244 for (int i = 0; i < host->max_clients && count < max_ids; i++) {
1245 if (host->clients[i].active) {
1246 ids[count++] = host->clients[i].client_id;
1247 }
1248 }
1249
1250 mutex_unlock(&host->clients_mutex);
1251 return count;
1252}
1253
1254/* ============================================================================
1255 * Session Host Broadcast Functions
1256 * ============================================================================ */
1257
1258asciichat_error_t session_host_broadcast_frame(session_host_t *host, const char *frame) {
1259 if (!host || !host->initialized || !frame) {
1260 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_broadcast_frame: invalid parameter");
1261 }
1262
1263 if (!host->running) {
1264 return SET_ERRNO(ERROR_INVALID_STATE, "session_host_broadcast_frame: not running");
1265 }
1266
1267 // Broadcast ASCII frame to all connected clients
1268 size_t frame_len = strlen(frame) + 1; // Include null terminator
1269 asciichat_error_t result = ASCIICHAT_OK;
1270
1271 mutex_lock(&host->clients_mutex);
1272 for (int i = 0; i < host->max_clients; i++) {
1273 if (host->clients[i].active && host->clients[i].socket != INVALID_SOCKET_VALUE) {
1274 asciichat_error_t send_result =
1275 packet_send(host->clients[i].socket, PACKET_TYPE_ASCII_FRAME, (const void *)frame, frame_len);
1276 if (send_result != ASCIICHAT_OK) {
1277 log_warn("Failed to send ASCII frame to client %u", host->clients[i].client_id);
1278 result = send_result; // Store error but continue broadcasting to other clients
1279 }
1280 }
1281 }
1282 mutex_unlock(&host->clients_mutex);
1283
1284 return result;
1285}
1286
1287asciichat_error_t session_host_send_frame(session_host_t *host, uint32_t client_id, const char *frame) {
1288 if (!host || !host->initialized || !frame) {
1289 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_send_frame: invalid parameter");
1290 }
1291
1292 if (!host->running) {
1293 return SET_ERRNO(ERROR_INVALID_STATE, "session_host_send_frame: not running");
1294 }
1295
1296 // Send ASCII frame to specific client
1297 size_t frame_len = strlen(frame) + 1; // Include null terminator
1298
1299 mutex_lock(&host->clients_mutex);
1300 for (int i = 0; i < host->max_clients; i++) {
1301 if (host->clients[i].client_id == client_id && host->clients[i].active &&
1302 host->clients[i].socket != INVALID_SOCKET_VALUE) {
1303 asciichat_error_t result =
1304 packet_send(host->clients[i].socket, PACKET_TYPE_ASCII_FRAME, (const void *)frame, frame_len);
1305 mutex_unlock(&host->clients_mutex);
1306 return result;
1307 }
1308 }
1309 mutex_unlock(&host->clients_mutex);
1310
1311 return SET_ERRNO(ERROR_NOT_FOUND, "session_host_send_frame: client %u not found", client_id);
1312}
1313
1314/* ============================================================================
1315 * Session Host Render Thread Functions
1316 * ============================================================================ */
1317
1319 if (!host || !host->initialized) {
1320 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_start_render: invalid host");
1321 }
1322
1323 if (!host->running) {
1324 return SET_ERRNO(ERROR_INVALID_STATE, "session_host_start_render: not running");
1325 }
1326
1327 if (host->render_thread_running) {
1328 return ASCIICHAT_OK; // Already running
1329 }
1330
1331 // Create audio context for mixing (host mode = true)
1332 if (!host->audio_ctx) {
1333 host->audio_ctx = session_audio_create(true);
1334 if (!host->audio_ctx) {
1335 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to create audio context");
1336 }
1337 }
1338
1339 // Create Opus decoder for decoding incoming Opus audio (48kHz)
1340 if (!host->opus_decoder) {
1342 if (!host->opus_decoder) {
1344 host->audio_ctx = NULL;
1345 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to create Opus decoder");
1346 }
1347 }
1348
1349 // Create Opus encoder for encoding mixed audio for broadcast (48kHz, VOIP mode, 24kbps)
1350 if (!host->opus_encoder) {
1351 host->opus_encoder = opus_codec_create_encoder(OPUS_APPLICATION_VOIP, 48000, 24000);
1352 if (!host->opus_encoder) {
1354 host->opus_decoder = NULL;
1356 host->audio_ctx = NULL;
1357 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to create Opus encoder");
1358 }
1359 }
1360
1361 // Spawn render thread
1362 host->render_thread_running = true;
1363 if (asciichat_thread_create(&host->render_thread, host_render_thread, host) != 0) {
1364 log_error("Failed to spawn render thread");
1365 host->render_thread_running = false;
1366 return SET_ERRNO(ERROR_THREAD, "Failed to spawn render thread");
1367 }
1368
1369 log_info("Host render thread started");
1370 return ASCIICHAT_OK;
1371}
1372
1374 if (!host || !host->initialized) {
1375 return;
1376 }
1377
1378 if (!host->render_thread_running) {
1379 return;
1380 }
1381
1382 // Signal thread to stop
1383 host->render_thread_running = false;
1384
1385 // Wait for thread to complete
1387
1388 // Clean up audio resources
1389 if (host->audio_ctx) {
1391 host->audio_ctx = NULL;
1392 }
1393 if (host->opus_decoder) {
1395 host->opus_decoder = NULL;
1396 }
1397 if (host->opus_encoder) {
1399 host->opus_encoder = NULL;
1400 }
1401
1402 log_info("Host render thread stopped");
1403}
1404
1405/* ============================================================================
1406 * Session Host Transport Functions (WebRTC Integration)
1407 * ============================================================================ */
1408
1409asciichat_error_t session_host_set_client_transport(session_host_t *host, uint32_t client_id,
1410 acip_transport_t *transport) {
1411 if (!host || !host->initialized) {
1412 return SET_ERRNO(ERROR_INVALID_PARAM, "Host is NULL or not initialized");
1413 }
1414
1415 mutex_lock(&host->clients_mutex);
1416
1417 // Find client with matching ID
1418 for (int i = 0; i < host->max_clients; i++) {
1419 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1420 log_info("session_host_set_client_transport: Setting transport=%p for client %u (was=%p)", transport, client_id,
1421 host->clients[i].transport);
1422
1423 host->clients[i].transport = transport;
1424
1425 if (transport) {
1426 log_info("WebRTC transport now active for client %u", client_id);
1427 } else {
1428 log_info("WebRTC transport cleared for client %u, reverting to socket", client_id);
1429 }
1430
1431 mutex_unlock(&host->clients_mutex);
1432 return ASCIICHAT_OK;
1433 }
1434 }
1435
1436 mutex_unlock(&host->clients_mutex);
1437 log_warn("Client %u not found", client_id);
1438 return SET_ERRNO(ERROR_NOT_FOUND, "Client not found");
1439}
1440
1441acip_transport_t *session_host_get_client_transport(session_host_t *host, uint32_t client_id) {
1442 if (!host || !host->initialized) {
1443 return NULL;
1444 }
1445
1446 mutex_lock(&host->clients_mutex);
1447
1448 // Find client with matching ID
1449 for (int i = 0; i < host->max_clients; i++) {
1450 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1451 acip_transport_t *transport = host->clients[i].transport;
1452 mutex_unlock(&host->clients_mutex);
1453 return transport;
1454 }
1455 }
1456
1457 mutex_unlock(&host->clients_mutex);
1458 return NULL;
1459}
1460
1461bool session_host_client_has_transport(session_host_t *host, uint32_t client_id) {
1462 if (!host || !host->initialized) {
1463 return false;
1464 }
1465
1466 mutex_lock(&host->clients_mutex);
1467
1468 // Find client with matching ID
1469 for (int i = 0; i < host->max_clients; i++) {
1470 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1471 bool has_transport = host->clients[i].transport != NULL;
1472 mutex_unlock(&host->clients_mutex);
1473 return has_transport;
1474 }
1475 }
1476
1477 mutex_unlock(&host->clients_mutex);
1478 return false;
1479}
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])
Definition ascii.c:69
char * ascii_create_grid(ascii_frame_source_t *sources, int source_count, int width, int height, size_t *out_size)
Definition ascii.c:542
char g_default_luminance_palette[256]
Definition ascii_simd.c:35
void session_host_stop_render(session_host_t *host)
Definition host.c:1373
asciichat_error_t session_host_inject_audio(session_host_t *host, uint32_t participant_id, const float *samples, size_t count)
Definition host.c:1117
bool session_host_client_has_transport(session_host_t *host, uint32_t client_id)
Definition host.c:1461
bool session_host_is_running(session_host_t *host)
Definition host.c:917
struct session_host session_host_t
Internal session host structure.
asciichat_error_t session_host_inject_frame(session_host_t *host, uint32_t participant_id, const image_t *frame)
Definition host.c:1072
uint32_t session_host_add_memory_participant(session_host_t *host)
Definition host.c:996
uint32_t session_host_add_client(session_host_t *host, socket_t socket, const char *ip, int port)
Definition host.c:928
void session_host_stop(session_host_t *host)
Definition host.c:857
acip_transport_t * session_host_get_client_transport(session_host_t *host, uint32_t client_id)
Definition host.c:1441
asciichat_error_t session_host_send_frame(session_host_t *host, uint32_t client_id, const char *frame)
Definition host.c:1287
asciichat_error_t session_host_set_client_transport(session_host_t *host, uint32_t client_id, acip_transport_t *transport)
Definition host.c:1409
int session_host_get_client_count(session_host_t *host)
Definition host.c:1229
asciichat_error_t session_host_remove_client(session_host_t *host, uint32_t client_id)
Definition host.c:1160
asciichat_error_t session_host_find_client(session_host_t *host, uint32_t client_id, session_host_client_info_t *info)
Definition host.c:1204
asciichat_error_t session_host_start(session_host_t *host)
Definition host.c:797
int session_host_get_client_ids(session_host_t *host, uint32_t *ids, int max_ids)
Definition host.c:1236
#define SESSION_HOST_DEFAULT_MAX_CLIENTS
Default maximum clients.
Definition host.c:43
asciichat_error_t session_host_broadcast_frame(session_host_t *host, const char *frame)
Definition host.c:1258
void session_host_destroy(session_host_t *host)
Definition host.c:222
session_host_t * session_host_create(const session_host_config_t *config)
Definition host.c:166
asciichat_error_t session_host_start_render(session_host_t *host)
Definition host.c:1318
int socket_t
void session_audio_destroy(session_audio_ctx_t *ctx)
session_audio_ctx_t * session_audio_create(bool is_host)
opus_codec_t * opus_codec_create_decoder(int sample_rate)
Definition opus_codec.c:62
int opus_codec_decode(opus_codec_t *codec, const uint8_t *data, size_t data_len, float *out_samples, int out_num_samples)
Definition opus_codec.c:128
opus_codec_t * opus_codec_create_encoder(opus_application_t application, int sample_rate, int bitrate)
Definition opus_codec.c:18
void opus_codec_destroy(opus_codec_t *codec)
Definition opus_codec.c:215
size_t opus_codec_encode(opus_codec_t *codec, const float *samples, int num_samples, uint8_t *out_data, size_t out_size)
Definition opus_codec.c:97
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:348
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)
Definition packet.c:1133
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:288
void platform_sleep_ms(unsigned int ms)
ringbuffer_t * ringbuffer_create(size_t element_size, size_t capacity)
Definition ringbuffer.c:28
void ringbuffer_destroy(ringbuffer_t *rb)
Definition ringbuffer.c:54
bool ringbuffer_read(ringbuffer_t *rb, void *data)
Definition ringbuffer.c:83
bool ringbuffer_write(ringbuffer_t *rb, const void *data)
Definition ringbuffer.c:61
uint8_t participant_id[16]
Internal client record structure.
Definition host.c:52
ringbuffer_t * incoming_audio
Incoming audio ringbuffer (written by receive loop, read by render thread)
Definition host.c:70
char ip_address[64]
Definition host.c:56
socket_t socket
Definition host.c:55
uint32_t client_id
Definition host.c:54
participant_type_t participant_type
Definition host.c:53
uint64_t connected_at
Definition host.c:61
image_t * incoming_video
Incoming video frame buffer (for host render thread)
Definition host.c:67
struct acip_transport * transport
Alternative transport (WebRTC, WebSocket, etc.) - NULL if using socket only.
Definition host.c:64
Internal session host structure.
Definition host.c:82
bool running
Server is running.
Definition host.c:117
char ipv6_address[64]
IPv6 bind address.
Definition host.c:90
char password[256]
Password.
Definition host.c:102
char ipv4_address[64]
IPv4 bind address.
Definition host.c:87
mutex_t clients_mutex
Client list mutex.
Definition host.c:129
bool initialized
Context is initialized.
Definition host.c:159
opus_codec_t * opus_encoder
Opus encoder for encoding mixed audio for broadcast.
Definition host.c:156
asciichat_thread_t receive_thread
Receive thread handle.
Definition host.c:138
asciichat_thread_t render_thread
Render thread handle (for video mixing and audio distribution)
Definition host.c:144
int client_count
Current client count.
Definition host.c:123
socket_t socket_v6
IPv6 listen socket.
Definition host.c:114
session_host_client_t * clients
Client array.
Definition host.c:120
opus_codec_t * opus_decoder
Opus decoder for decoding incoming Opus audio.
Definition host.c:153
socket_t socket_v4
IPv4 listen socket.
Definition host.c:111
void * user_data
User data for callbacks.
Definition host.c:108
bool receive_thread_running
Receive thread is running.
Definition host.c:141
char key_path[512]
Key path.
Definition host.c:99
session_host_callbacks_t callbacks
Event callbacks.
Definition host.c:105
session_audio_ctx_t * audio_ctx
Audio context for mixing (host only)
Definition host.c:150
asciichat_thread_t accept_thread
Accept thread handle.
Definition host.c:132
int max_clients
Maximum clients.
Definition host.c:93
int port
Port to listen on.
Definition host.c:84
bool render_thread_running
Render thread is running.
Definition host.c:147
uint32_t next_client_id
Next client ID counter.
Definition host.c:126
bool encryption_enabled
Encryption enabled.
Definition host.c:96
bool accept_thread_running
Accept thread is running.
Definition host.c:135
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
int mutex_init(mutex_t *mutex)
Definition threading.c:16
int asciichat_thread_create(asciichat_thread_t *thread, void *(*start_routine)(void *), void *arg)
Definition threading.c:42
int asciichat_thread_join(asciichat_thread_t *thread, void **retval)
Definition threading.c:46
int mutex_destroy(mutex_t *mutex)
Definition threading.c:21
uint64_t time_get_ns(void)
Definition util/time.c:48
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Definition util/time.c:90
void image_destroy(image_t *p)
Definition video/image.c:85
image_t * image_new(size_t width, size_t height)
Definition video/image.c:36