ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches

🏠 Server-side session hosting abstraction More...

Go to the source code of this file.

Data Structures

struct  session_host_client_info_t
 Snapshot of the address, identity, activity, and connection time for one host client. More...
struct  session_host_callbacks_t
 Snapshot of the address, identity, activity, and connection time for one host client. More...
struct  session_host_config_t
 Host-side event hooks for joins, departures, media, and errors. More...
struct  session_host_render_config_t
 When code needs to work with session host render config, it keeps caps, palette, width, height here instead of scattering it across globals. Initialization establishes the handles and sizes; teardown must release any allocation, lock, thread, or callback context recorded in those fields. More...
struct  session_host_render_target_t
 session host render target is intentionally a small, concrete record. Fields such as client_id, context give callers the identity and current value they need. It is normally borrowed from, or owned by, the surrounding subsystem—not independently managed. Callbacks execute under the host client lock and must not call host APIs. More...

Typedefs

typedef struct acip_transport acip_transport_t
 Forward declaration of ACIP transport.

Functions

session_host_t * session_host_create (const session_host_config_t *config)
 Create a new session host.
asciichat_error_t session_host_destroy (session_host_t *host)
 Destroy session host and free resources.
asciichat_error_t session_host_start (session_host_t *host)
 Start accepting client connections.
asciichat_error_t session_host_stop (session_host_t *host)
 Stop accepting connections and disconnect all clients.
bool session_host_is_running (session_host_t *host)
 Check if host is running.
uint32_t session_host_add_client (session_host_t *host, acip_transport_t *transport, acip_transport_ownership_t ownership, const char *ip, int port)
uint32_t session_host_add_memory_participant (session_host_t *host)
 Add a memory participant (host's own media).
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.
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.
asciichat_error_t session_host_remove_client (session_host_t *host, uint32_t client_id)
 Remove a client by ID.
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.
int session_host_get_client_count (session_host_t *host)
 Get number of connected clients.
int session_host_get_client_ids (session_host_t *host, uint32_t *ids, int max_ids)
 Get list of all connected client IDs.
asciichat_error_t session_host_broadcast_frame (session_host_t *host, const char *frame)
 Broadcast ASCII frame to all clients.
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.
asciichat_error_t session_host_set_display (session_host_t *host, struct session_display_ctx *display)
 Set a native terminal display context for local frame rendering.
asciichat_error_t session_host_set_render_target (session_host_t *host, const session_host_render_target_t *target)
asciichat_error_t session_host_start_render (session_host_t *host)
 Start media rendering thread (video mixing and audio distribution).
void session_host_stop_render (session_host_t *host)
 Stop media rendering thread.
asciichat_error_t session_host_set_client_transport (session_host_t *host, uint32_t client_id, acip_transport_t *transport, acip_transport_ownership_t ownership)
 Select a transport for a specific client in the host.
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.
bool session_host_client_has_transport (session_host_t *host, uint32_t client_id)
 Check if a specific client has an active ACIP transport.
asciichat_error_t session_host_receive_packet (session_host_t *host, uint32_t client_id, packet_type_t type, const void *data, size_t len)
asciichat_error_t session_host_render_video (session_host_t *host)
asciichat_error_t session_host_accept_peer (session_host_t *, acip_transport_t *)
asciichat_error_t session_host_render_audio (session_host_t *host)

Detailed Description

🏠 Server-side session hosting abstraction

Definition in file host.h.

Function Documentation

◆ session_host_accept_peer()

asciichat_error_t session_host_accept_peer ( session_host_t * host,
acip_transport_t * transport )

Definition at line 1106 of file host.c.

1106 {
1107#ifndef __EMSCRIPTEN__
1108 if (!host || !host->network_server)
1109 return SET_ERRNO(ERROR_INVALID_STATE, "Host network is not running");
1110 return network_server_adopt(host->network_server, transport, "peer", true);
1111#else
1112 (void)host;
1113 (void)transport;
1114 return SET_ERRNO(ERROR_NOT_SUPPORTED, "Browser peers use the browser host bridge");
1115#endif
1116}
#define SET_ERRNO(code,...)
@ ERROR_INVALID_STATE
@ ERROR_NOT_SUPPORTED
Definition error_codes.h:69
asciichat_error_t network_server_adopt(network_server_t *s, acip_transport_t *transport, const char *peer, bool secure)

References ERROR_INVALID_STATE, ERROR_NOT_SUPPORTED, network_server_adopt(), and SET_ERRNO.

◆ session_host_add_client()

uint32_t session_host_add_client ( session_host_t * host,
acip_transport_t * transport,
acip_transport_ownership_t ownership,
const char * ip,
int port )

Register a transport-backed client. The same API accepts TCP, WebSocket, and WebRTC. Ownership transfers only on success. NULL registers a participant whose transport will be supplied later. on_client_join runs after admission.

Definition at line 1129 of file host.c.

1130 {
1131 if (!host || !host->initialized) {
1132 return 0;
1133 }
1134
1135 if ((ownership != ACIP_TRANSPORT_BORROWED && ownership != ACIP_TRANSPORT_OWNED) ||
1136 (!transport && ownership == ACIP_TRANSPORT_OWNED)) {
1137 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid transport ownership");
1138 return 0;
1139 }
1140
1141 mutex_lock(&host->clients_mutex);
1142
1143 // Check if we have room
1144 if (host->client_count >= host->max_clients) {
1145 mutex_unlock(&host->clients_mutex);
1146 SET_ERRNO(ERROR_SESSION_FULL, "Maximum clients reached");
1147 return 0;
1148 }
1149
1150 // Find empty slot
1151 for (int i = 0; i < host->max_clients; i++) {
1152 if (!host->clients[i].active) {
1153 host->clients[i].participant_type = PARTICIPANT_TYPE_NETWORK;
1154 host->clients[i].client_id = host->next_client_id++;
1155 host->clients[i].connection = NULL;
1156 if (ip) {
1157 SAFE_STRNCPY(host->clients[i].ip_address, ip, sizeof(host->clients[i].ip_address));
1158 }
1159 host->clients[i].port = port;
1160 host->clients[i].active = true;
1161 host->clients[i].video_active = false;
1162 host->clients[i].audio_active = false;
1163 host->clients[i].connected_at = (uint64_t)time(NULL);
1164 host->clients[i].transport = NULL;
1165 host->clients[i].transport_ownership = ACIP_TRANSPORT_BORROWED;
1166 host->clients[i].render_width = 0;
1167 host->clients[i].render_height = 0;
1168 memset(&host->clients[i].rendering, 0, sizeof(host->clients[i].rendering));
1169 host->clients[i].rendering.fps_limit = DEFAULT_MAX_FPS;
1170 host->clients[i].next_video_deadline = 0;
1171
1172 // Allocate media buffers
1173 host->clients[i].incoming_video = image_new(480, 270); // Network-optimal size (HD preview)
1174 host->clients[i].incoming_audio = ringbuffer_create(
1175 sizeof(float), (OPUS_FRAME_SAMPLES * AUDIO_CHANNELS) * HOST_AUDIO_BUFFER_FRAMES); // ~200ms buffer @ 48kHz
1176
1177 if (!host->clients[i].incoming_video || !host->clients[i].incoming_audio) {
1178 // Cleanup on allocation failure
1179 if (host->clients[i].incoming_video) {
1180 image_destroy(host->clients[i].incoming_video);
1181 host->clients[i].incoming_video = NULL;
1182 }
1183 if (host->clients[i].incoming_audio) {
1184 ringbuffer_destroy(host->clients[i].incoming_audio);
1185 host->clients[i].incoming_audio = NULL;
1186 }
1187 host->clients[i].active = false;
1188 mutex_unlock(&host->clients_mutex);
1189 SET_ERRNO(ERROR_MEMORY, "Failed to allocate media buffers for client");
1190 return 0;
1191 }
1192
1193 host->clients[i].transport = transport;
1194 host->clients[i].transport_ownership = ownership;
1195 uint32_t client_id = host->clients[i].client_id;
1196 host->client_count++;
1197
1198 mutex_unlock(&host->clients_mutex);
1199
1200 // Invoke callback
1201 if (host->callbacks.on_client_join) {
1202 host->callbacks.on_client_join(host, client_id, host->user_data);
1203 }
1204
1205 return client_id;
1206 }
1207 }
1208
1209 mutex_unlock(&host->clients_mutex);
1210 return 0;
1211}
#define OPUS_FRAME_SAMPLES
#define AUDIO_CHANNELS
Definition audio/limits.h:5
unsigned int uint32_t
Definition common.h:62
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
unsigned long long uint64_t
Definition common.h:63
@ ERROR_MEMORY
Definition error_codes.h:51
@ ERROR_SESSION_FULL
Definition error_codes.h:82
@ ERROR_INVALID_PARAM
#define DEFAULT_MAX_FPS
Default maximum frame rate (frames per second).
ringbuffer_t * ringbuffer_create(size_t element_size, size_t capacity)
Create a new ring buffer.
Definition ringbuffer.c:34
void ringbuffer_destroy(ringbuffer_t *rb)
Destroy a ring buffer and free its memory.
Definition ringbuffer.c:61
void image_destroy(image_t *image)
Destroy an image allocated with image_new().
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
#define HOST_AUDIO_BUFFER_FRAMES
Definition host.c:5

References ACIP_TRANSPORT_BORROWED, ACIP_TRANSPORT_OWNED, AUDIO_CHANNELS, DEFAULT_MAX_FPS, ERROR_INVALID_PARAM, ERROR_MEMORY, ERROR_SESSION_FULL, HOST_AUDIO_BUFFER_FRAMES, image_destroy(), image_new(), OPUS_FRAME_SAMPLES, PARTICIPANT_TYPE_NETWORK, ringbuffer_create(), ringbuffer_destroy(), SAFE_STRNCPY, and SET_ERRNO.

Referenced by add_transport_client().

◆ session_host_receive_packet()

asciichat_error_t session_host_receive_packet ( session_host_t * host,
uint32_t client_id,
packet_type_t type,
const void * data,
size_t len )

Process a packet delivered by an event-driven transport.

Definition at line 343 of file host.c.

344 {
345 if (!host || !host->initialized || (!data && len))
346 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid host packet");
347 acip_transport_t *client_transport = session_host_get_client_transport(host, client_id);
348 switch (ptype) {
350 // Client sent a video frame - parse and store in incoming buffer
351 if (data && len >= sizeof(image_frame_packet_t)) {
352 const image_frame_packet_t *frame_hdr = (const image_frame_packet_t *)data;
353 const uint8_t *pixel_data = (const uint8_t *)data + sizeof(image_frame_packet_t);
354 size_t pixel_data_size = len - sizeof(image_frame_packet_t);
355 uint32_t width = NET_TO_HOST_U32(frame_hdr->width);
357 uint32_t pixel_format = NET_TO_HOST_U32(frame_hdr->pixel_format);
358 size_t bytes_per_pixel = (pixel_format == PIXEL_FORMAT_RGBA || pixel_format == PIXEL_FORMAT_BGRA) ? 4 : 3;
359 size_t expected_size = 0;
360 size_t pixel_count = 0;
361
362 if (pixel_format < PIXEL_FORMAT_RGB || pixel_format > PIXEL_FORMAT_BGRA || width == 0 || height == 0 ||
363 width > INT_MAX || height > INT_MAX ||
364 checked_size_mul((size_t)width, (size_t)height, &pixel_count) != ASCIICHAT_OK ||
365 checked_size_mul(pixel_count, bytes_per_pixel, &expected_size) != ASCIICHAT_OK ||
366 expected_size != pixel_data_size) {
367 log_warn("Discarding malformed video frame from client %u (%ux%u, format %u, payload %zu)", client_id, width,
368 height, pixel_format, pixel_data_size);
369 } else {
370 // Frame dimensions and pixel format are network-order fields. Resize
371 // this participant's image when its capture dimensions change.
372 mutex_lock(&host->clients_mutex);
373 for (int j = 0; j < host->max_clients; j++) {
374 if (host->clients[j].active && host->clients[j].client_id == client_id && host->clients[j].incoming_video) {
375 image_t *img = host->clients[j].incoming_video;
376 if (img->w != (int)width || img->h != (int)height) {
377 image_t *resized = image_new(width, height);
378 if (!resized) {
379 log_warn("Could not allocate %ux%u video frame for client %u", width, height, client_id);
380 break;
381 }
382 image_destroy(img);
383 host->clients[j].incoming_video = img = resized;
384 }
385
386 uint8_t *dst = (uint8_t *)img->pixels;
387 if (pixel_format == PIXEL_FORMAT_RGB) {
388 memcpy(dst, pixel_data, pixel_count * 3);
389 } else if (pixel_format == PIXEL_FORMAT_BGR) {
390 for (size_t p = 0; p < pixel_count; p++) {
391 dst[p * 3] = pixel_data[p * 3 + 2];
392 dst[p * 3 + 1] = pixel_data[p * 3 + 1];
393 dst[p * 3 + 2] = pixel_data[p * 3];
394 }
395 } else {
396 const bool bgra = pixel_format == PIXEL_FORMAT_BGRA;
397 for (size_t p = 0; p < pixel_count; p++) {
398 const uint8_t *src = pixel_data + p * 4;
399 dst[p * 3] = src[bgra ? 2 : 0];
400 dst[p * 3 + 1] = src[1];
401 dst[p * 3 + 2] = src[bgra ? 0 : 2];
402 }
403 }
404 log_debug_every(500 * US_PER_MS_INT, "Frame received from client %u (%ux%u, format %u)", client_id, width,
405 height, pixel_format);
406 break;
407 }
408 }
409 mutex_unlock(&host->clients_mutex);
410 }
411 }
412 break;
413
415 // Client sent Opus-encoded audio batch
416 if (data && len > 16) { // Must have at least header
417 const uint8_t *batch_data = (const uint8_t *)data;
418 // Parse header: sample_rate (4), frame_duration (4), frame_count (4), reserved (4)
419 (void)batch_data[0]; // Avoid unused variable warning if we don't use sample_rate/frame_duration
420 uint32_t batch_frame_count_net = 0;
421 memcpy(&batch_frame_count_net, batch_data + 8, sizeof(batch_frame_count_net));
422 uint32_t batch_frame_count = NET_TO_HOST_U32(batch_frame_count_net);
423
424 size_t frame_sizes_len = (size_t)batch_frame_count * sizeof(uint16_t);
425 if (batch_frame_count > 0 && batch_frame_count <= 1000 && 16 + frame_sizes_len <= len) {
426 const uint8_t *frame_sizes = batch_data + 16;
427 const uint8_t *opus_frames = batch_data + 16 + (batch_frame_count * sizeof(uint16_t));
428 const uint8_t *batch_end = batch_data + len;
429
430 // Find client and decode audio
431 mutex_lock(&host->clients_mutex);
432 for (int j = 0; j < host->max_clients; j++) {
433 if (host->clients[j].client_id == client_id && host->clients[j].active && host->clients[j].incoming_audio) {
434 if (!host->clients[j].opus_decoder)
436 if (!host->clients[j].opus_decoder) {
437 mutex_unlock(&host->clients_mutex);
438 return SET_ERRNO(ERROR_AUDIO, "Failed to initialize participant decoder");
439 }
440 // Each participant owns an independent predictive Opus stream.
441 const uint8_t *current_frame = opus_frames;
442 for (uint32_t k = 0; k < batch_frame_count; k++) {
443 uint16_t frame_size_net = 0;
444 memcpy(&frame_size_net, frame_sizes + k * sizeof(frame_size_net), sizeof(frame_size_net));
445 uint16_t frame_size = NET_TO_HOST_U16(frame_size_net);
446 if ((size_t)(batch_end - current_frame) < frame_size) {
447 break;
448 }
449 if (frame_size > 0) {
450 // Allocate buffer for decoded samples
451 float decoded_samples[(OPUS_FRAME_SAMPLES * AUDIO_CHANNELS)]; // Max 20ms @ 48kHz
452 int decoded_count = opus_codec_decode(host->clients[j].opus_decoder, current_frame, (int)frame_size,
453 decoded_samples, (OPUS_FRAME_SAMPLES * AUDIO_CHANNELS));
454 if (decoded_count > 0) {
455 host->clients[j].audio_active = true;
456 // Write samples to ringbuffer one at a time
457 for (int s = 0; s < decoded_count; s++) {
458 ringbuffer_write(host->clients[j].incoming_audio, &decoded_samples[s]);
459 }
460 }
461 }
462 current_frame += frame_size;
463 }
464 log_debug_every(NS_PER_MS_INT, "Audio batch received from client %u (%u frames)", client_id,
465 batch_frame_count);
466 break;
467 }
468 }
469 mutex_unlock(&host->clients_mutex);
470 }
471 }
472 break;
473
475 log_info("Client %u started streaming", client_id);
476 mutex_lock(&host->clients_mutex);
477 for (int j = 0; j < host->max_clients; j++) {
478 if (host->clients[j].client_id == client_id) {
479 host->clients[j].video_active = true;
480 break;
481 }
482 }
483 mutex_unlock(&host->clients_mutex);
484 break;
485
489 size_t reply_length = 0;
491 session_host_client_t *target = NULL;
492 mutex_lock(&host->clients_mutex);
493 for (int j = 0; j < host->max_clients; ++j) {
494 if (host->clients[j].active && host->clients[j].client_id == client_id) {
495 target = &host->clients[j];
496 break;
497 }
498 }
499 if (!target)
500 result = SET_ERRNO(ERROR_INVALID_STATE, "Unknown settings participant");
501 else if (ptype == PACKET_TYPE_CLIENT_CAPABILITIES) {
502 if (!data || len != sizeof(terminal_capabilities_packet_t))
503 result = SET_ERRNO(ERROR_INVALID_PARAM, "Invalid capabilities");
504 else {
506 memcpy(&packet, data, sizeof(packet));
507 if (target->rendering.width) {
508 // Codec advertisements cannot change an established viewer's settings.
509 mutex_unlock(&host->clients_mutex);
510 return ASCIICHAT_OK;
511 }
512 result = acip_render_settings_capabilities(&target->rendering, &packet);
513 if (result == ASCIICHAT_OK) {
515 size_t query_length;
516 acip_settings_header_t header = {.schema = 1, .expected_revision = UINT32_MAX};
517 result = acip_settings_encode(&header, "", query, sizeof(query), &query_length);
518 if (result == ASCIICHAT_OK)
519 result =
520 acip_settings_request(&target->rendering, query, query_length, reply, sizeof(reply), &reply_length);
521 }
522 }
523 } else if (!target->rendering.revision)
524 result = SET_ERRNO(ERROR_INVALID_STATE, "Settings received before capabilities");
525 else
526 result = acip_settings_request(&target->rendering, data, len, reply, sizeof(reply), &reply_length);
527 if (target && result == ASCIICHAT_OK) {
528 target->render_width = target->rendering.width;
529 target->render_height = target->rendering.height;
530 if (reply_length)
531 result = packet_send_via_transport(target->transport, PACKET_TYPE_ACIP_SETTINGS_ACK, reply, reply_length, 0);
532 }
533 mutex_unlock(&host->clients_mutex);
534 return result;
535 }
536
538 log_info("Client %u stopped streaming", client_id);
539 mutex_lock(&host->clients_mutex);
540 for (int j = 0; j < host->max_clients; j++) {
541 if (host->clients[j].client_id == client_id) {
542 host_forget_render_history(host, client_id);
543 host->clients[j].video_active = false;
544 host->clients[j].audio_active = false;
545 break;
546 }
547 }
548 mutex_unlock(&host->clients_mutex);
549 break;
550
551 case PACKET_TYPE_PING:
552 // Respond with PONG
553 log_debug_every(NS_PER_MS_INT, "PING from client %u", client_id);
554 acip_send_pong(client_transport);
555 break;
556
558 log_info("Client %u requested disconnect", client_id);
559 // Mark for removal (will be handled separately to avoid deadlock)
560 break;
561
562 default:
563 log_warn("Unknown packet type %u from client %u", ptype, client_id);
564 break;
565 }
566
567 return ASCIICHAT_OK;
568}
#define AUDIO_SAMPLE_RATE
Definition audio/limits.h:4
#define NET_TO_HOST_U16(val)
Definition endian.h:111
#define NET_TO_HOST_U32(val)
Definition endian.h:81
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
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:176
opus_codec_t * opus_codec_create_decoder_channels(int sample_rate, int channels)
Definition opus.c:92
unsigned short uint16_t
Definition common.h:61
unsigned char uint8_t
Definition common.h:60
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_AUDIO
Definition error_codes.h:62
@ ASCIICHAT_OK
Definition error_codes.h:46
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define NS_PER_MS_INT
Definition time.h:168
#define US_PER_MS_INT
Definition time.h:172
@ PACKET_TYPE_AUDIO_OPUS_BATCH
Batched Opus-encoded audio frames.
Definition packet.h:398
@ PACKET_TYPE_CLIENT_LEAVE
Clean disconnect notification.
Definition packet.h:413
@ PACKET_TYPE_IMAGE_FRAME
Complete RGB image with dimensions.
Definition packet.h:385
@ PACKET_TYPE_STREAM_START
Client requests to start sending video/audio.
Definition packet.h:415
@ PACKET_TYPE_ACIP_SETTINGS_SYNC
Settings sync (Initiator -> Host -> All Participants).
Definition packet.h:485
@ PACKET_TYPE_ACIP_SETTINGS_ACK
Settings acknowledgment (Participant -> Initiator).
Definition packet.h:487
@ PACKET_TYPE_CLIENT_CAPABILITIES
Client reports terminal capabilities.
Definition packet.h:405
@ PACKET_TYPE_PING
Keepalive ping packet.
Definition packet.h:407
@ PACKET_TYPE_STREAM_STOP
Client stops sending media.
Definition packet.h:417
#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.
bool ringbuffer_write(ringbuffer_t *rb, const void *data)
Try to write an element to the ring buffer (non-blocking).
Definition ringbuffer.c:71
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:1756
struct acip_transport acip_transport_t
Forward declaration of ACIP transport.
Definition host.h:506
asciichat_error_t acip_render_settings_capabilities(acip_render_settings_t *state, const terminal_capabilities_packet_t *packet)
asciichat_error_t acip_settings_encode(const acip_settings_header_t *header, const char *json, uint8_t *buffer, size_t capacity, size_t *length)
asciichat_error_t acip_settings_request(acip_render_settings_t *state, const void *payload, size_t length, uint8_t *reply, size_t capacity, size_t *reply_length)
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:749
asciichat_error_t acip_send_pong(acip_transport_t *transport)
Send pong packet.
Definition send.c:395
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)
Definition send.c:190
acip settings header keeps the values that move through the subsystem: schema, request,...
Describes one compressed image sent through ACIP. width, height, and pixel_format tell the decoder ho...
Definition packet.h:931
uint32_t pixel_format
Pixel format enum (1=RGB24, 2=RGBA32, 3=BGR24, 4=BGRA32).
Definition packet.h:937
uint32_t height
Image height in pixels.
Definition packet.h:935
uint32_t width
Image width in pixels.
Definition packet.h:933
SIMD-friendly storage for one RGB pixel. r, g, and b hold the color channels and padding keeps the la...
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).
Keeps the host-side rendering history used to compare successive frames and stabilize output for conn...
Definition host.c:88
acip_render_settings_t rendering
Definition host.c:100
uint16_t render_height
Definition host.c:99
uint16_t render_width
Definition host.c:98
struct acip_transport * transport
Transport borrowed from the connection owner or browser bridge.
Definition host.c:106
Terminal capabilities packet structure (Packet Type 5).
Definition packet.h:1074

References acip_render_settings_capabilities(), acip_send_pong(), acip_settings_encode(), ACIP_SETTINGS_HEADER_SIZE, ACIP_SETTINGS_MAX_SIZE, acip_settings_request(), ASCIICHAT_OK, AUDIO_CHANNELS, AUDIO_SAMPLE_RATE, ERROR_AUDIO, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, image_t::h, acip_render_settings_t::height, height, image_frame_packet_t::height, image_destroy(), image_new(), log_debug_every, log_info, log_warn, NET_TO_HOST_U16, NET_TO_HOST_U32, NS_PER_MS_INT, opus_codec_create_decoder_channels(), opus_codec_decode(), OPUS_FRAME_SAMPLES, packet_send_via_transport(), PACKET_TYPE_ACIP_SETTINGS_ACK, PACKET_TYPE_ACIP_SETTINGS_SYNC, PACKET_TYPE_AUDIO_OPUS_BATCH, PACKET_TYPE_CLIENT_CAPABILITIES, PACKET_TYPE_CLIENT_LEAVE, PACKET_TYPE_IMAGE_FRAME, PACKET_TYPE_PING, PACKET_TYPE_STREAM_START, PACKET_TYPE_STREAM_STOP, image_frame_packet_t::pixel_format, PIXEL_FORMAT_BGR, PIXEL_FORMAT_BGRA, PIXEL_FORMAT_RGB, PIXEL_FORMAT_RGBA, image_t::pixels, session_host_client_t::render_height, session_host_client_t::render_width, session_host_client_t::rendering, acip_render_settings_t::revision, ringbuffer_write(), session_host_get_client_transport(), SET_ERRNO, session_host_client_t::transport, US_PER_MS_INT, image_t::w, acip_render_settings_t::width, image_frame_packet_t::width, and width.

◆ session_host_render_audio()

asciichat_error_t session_host_render_audio ( session_host_t * host)

Mix and distribute one 20 ms audio frame without starting a thread.

Definition at line 817 of file host.c.

817 {
818 if (!host || !host->initialized)
819 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid host");
820 uint64_t mix_start = time_get_ns();
821 double total[(OPUS_FRAME_SAMPLES * AUDIO_CHANNELS)] = {0};
823 mutex_lock(&host->clients_mutex);
824 // Drain each source once. Keep unclipped samples so mix-minus can subtract
825 // the recipient before clipping without leaking its voice into the output.
826 for (int i = 0; i < host->max_clients; i++) {
827 session_host_client_t *client = &host->clients[i];
828 memset(client->audio_frame, 0, sizeof(client->audio_frame));
829 if (!client->active || !client->audio_active || !client->incoming_audio)
830 continue;
831 for (int j = 0; j < (OPUS_FRAME_SAMPLES * AUDIO_CHANNELS); j++) {
832 if (ringbuffer_read(client->incoming_audio, &client->audio_frame[j]))
833 total[j] += client->audio_frame[j];
834 }
835 }
836 for (int i = 0; i < host->max_clients; i++) {
837 session_host_client_t *client = &host->clients[i];
838 if (!client->active || !client->transport)
839 continue;
840 ASCII_OTEL_SCOPE(SERVER_AUDIO);
841 if (!client->opus_encoder)
842 client->opus_encoder =
844 if (!client->opus_encoder) {
845 result = SET_ERRNO(ERROR_AUDIO, "Failed to initialize participant encoder");
846 continue;
847 }
848 float mixed[(OPUS_FRAME_SAMPLES * AUDIO_CHANNELS)];
849 for (int j = 0; j < (OPUS_FRAME_SAMPLES * AUDIO_CHANNELS); j++) {
850 float sample = (float)(total[j] - client->audio_frame[j]);
851 mixed[j] = sample > 1.0f ? 1.0f : sample < -1.0f ? -1.0f : sample;
852 }
854 size_t opus_len = opus_codec_encode(client->opus_encoder, mixed, OPUS_FRAME_SAMPLES * AUDIO_CHANNELS, opus_buffer,
855 sizeof(opus_buffer));
856 if (opus_len > 0) {
857 uint16_t frame_sizes[1] = {(uint16_t)opus_len};
858 asciichat_error_t sent = acip_send_audio_opus_batch(client->transport, opus_buffer, opus_len, frame_sizes, 1,
860 if (sent != ASCIICHAT_OK)
861 log_warn("Failed to send mixed audio to client %u", client->client_id);
862 }
863 }
864 mutex_unlock(&host->clients_mutex);
866#ifndef __EMSCRIPTEN__
867 float recorded[(OPUS_FRAME_SAMPLES * AUDIO_CHANNELS)];
868 for (int j = 0; j < (OPUS_FRAME_SAMPLES * AUDIO_CHANNELS); j++)
869 recorded[j] = total[j] > 1.0 ? 1.0f : total[j] < -1.0 ? -1.0f : (float)total[j];
871#endif
872 return result;
873}
#define CLIENT_AUDIO_PIPELINE_FRAME_MS
#define AUDIO_OPUS_SESSION_PACKET_CAPACITY
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:133
opus_codec_t * opus_codec_create_encoder_channels(opus_application_t application, int sample_rate, int bitrate, int channels)
Definition opus.c:39
@ OPUS_APPLICATION_AUDIO
General audio (optimized for music).
Definition opus.h:86
bool ringbuffer_read(ringbuffer_t *rb, void *data)
Try to read an element from the ring buffer (non-blocking).
Definition ringbuffer.c:93
void stats_duration_record(stats_scope_t *scope, stats_duration_id_t id, uint64_t elapsed_ns)
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
void audio_recording_submit(audio_recording_source_t source, const float *samples, size_t count, uint64_t start_ns)
Definition recording.c:79
@ AUDIO_RECORDING_REMOTE
Definition recording.h:12
stats_scope_t * stats_runtime_scope(void)
Definition runtime.c:80
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:350
ringbuffer_t * incoming_audio
Incoming audio ringbuffer (written by receive loop, read by render thread).
Definition host.c:113
float audio_frame[(OPUS_FRAME_SAMPLES *AUDIO_CHANNELS)]
Definition host.c:116
opus_codec_t * opus_encoder
Definition host.c:115
uint32_t client_id
Definition host.c:90

References acip_send_audio_opus_batch(), session_host_client_t::active, ASCII_OTEL_SCOPE, ASCIICHAT_OK, session_host_client_t::audio_active, AUDIO_CHANNELS, session_host_client_t::audio_frame, AUDIO_OPUS_SESSION_PACKET_CAPACITY, AUDIO_RECORDING_REMOTE, audio_recording_submit(), AUDIO_SAMPLE_RATE, CLIENT_AUDIO_PIPELINE_FRAME_MS, session_host_client_t::client_id, ERROR_AUDIO, ERROR_INVALID_PARAM, session_host_client_t::incoming_audio, log_warn, OPUS_APPLICATION_AUDIO, opus_codec_create_encoder_channels(), opus_codec_encode(), session_host_client_t::opus_encoder, OPUS_FRAME_SAMPLES, ringbuffer_read(), SET_ERRNO, STATS_DURATION_AUDIO_MIX, stats_duration_record(), stats_runtime_scope(), and session_host_client_t::transport.

◆ session_host_render_video()

asciichat_error_t session_host_render_video ( session_host_t * host)

Run one shared host video cycle without starting a thread.

Definition at line 810 of file host.c.

810 {
811 if (!host || !host->initialized)
812 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid session host");
813 host_render_video(host, false);
814 return ASCIICHAT_OK;
815}

References ASCIICHAT_OK, ERROR_INVALID_PARAM, and SET_ERRNO.

◆ session_host_set_render_target()

asciichat_error_t session_host_set_render_target ( session_host_t * host,
const session_host_render_target_t * target )

Set local output on native or WASM hosts. NULL disables the local target. A client target is rendered once and delivered through its existing transport. A backend's context must outlive its registration.

Definition at line 1550 of file host.c.

1550 {
1551 if (!host || !host->initialized) {
1552 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid host");
1553 }
1554 if (target) {
1555 bool has_backend = target->get_config || target->render_frame;
1556 bool complete_backend = target->get_config && target->render_frame;
1557 bool valid_target = target->client_id ? !has_backend : complete_backend;
1558 if (!valid_target)
1559 return SET_ERRNO(ERROR_INVALID_PARAM, "Local rendering requires a client or a complete display backend");
1560 }
1561
1562 mutex_lock(&host->clients_mutex);
1563 if (target && target->client_id) {
1564 bool found = false;
1565 for (int i = 0; i < host->max_clients; ++i)
1566 if (host->clients[i].active && host->clients[i].client_id == target->client_id)
1567 found = true;
1568 if (!found) {
1569 mutex_unlock(&host->clients_mutex);
1570 return SET_ERRNO(ERROR_NOT_FOUND, "Local display client not found: %u", target->client_id);
1571 }
1572 }
1573 host->render_target = target ? *target : (session_host_render_target_t){0};
1574 mutex_unlock(&host->clients_mutex);
1575 return ASCIICHAT_OK;
1576}
@ ERROR_NOT_FOUND
session host render target is intentionally a small, concrete record. Fields such as client_id,...
Definition host.h:450
void(* render_frame)(void *context, const char *frame, size_t size, int width, int height)
Definition host.h:453
asciichat_error_t(* get_config)(void *context, session_host_render_config_t *config)
Definition host.h:452

References ASCIICHAT_OK, session_host_render_target_t::client_id, ERROR_INVALID_PARAM, ERROR_NOT_FOUND, session_host_render_target_t::get_config, session_host_render_target_t::render_frame, and SET_ERRNO.