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

🎬 Multi-client video mixer: frame generation, ASCII conversion, and per-client personalized rendering More...

Go to the source code of this file.

Data Structures

struct  image_source_t
 One client's video input as seen by the compositor. More...

Macros

#define SERVER_STREAM_HEX_BUFFER_SIZE   300
#define SERVER_STREAM_ASCII_BUFFER_SIZE   100

Functions

char * create_mixed_ascii_frame_for_client (const char *target_client_id, unsigned short width, unsigned short height, bool wants_stretch, size_t *out_size, bool *out_grid_changed, int *out_sources_count, acip_render_settings_t *out_rendering)
 Generate personalized ASCII frame for a specific client.
bool any_clients_sending_video (void)
 Queue ASCII frame for delivery to specific client.

Detailed Description

🎬 Multi-client video mixer: frame generation, ASCII conversion, and per-client personalized rendering

1. Collect video frames from all active clientsclientsCreate composite video layouts (single client, 2x2, 3x3 grids))Generate client-specific ASCII art with terminal capability awarenessawarenessProcess latest frames from double-buffer system for real-time performanceperformanceManage memory efficiently with owned buffers and zero-copy operationsoperationsSupport advanced rendering modes (half-block, color, custom palettes) VIDEO MIXING ARCHITECTURE:

The mixing system operates in several stages:

  1. FRAME COLLECTION:
    • Scans all active clients for available video frames
    • Uses per-client double-buffer system for smooth frame handling
    • Implements aggressive frame dropping to maintain real-time performance
    • Always uses the latest available frame for professional-grade performance
  2. LAYOUT CALCULATION:
    • Determines grid size based on number of active video sources
    • Supports: single (1x1), side-by-side (2x1), 2x2, 3x2, 3x3 layouts
    • Calculates aspect-ratio preserving dimensions for each cell
  3. COMPOSITE GENERATION:
    • Creates composite image with proper aspect ratio handling
    • Places each client's video in appropriate grid cell
    • Supports both normal and half-block rendering modes
  4. ASCII CONVERSION:
    • Converts composite to ASCII using client-specific capabilities
    • Respects terminal color support, palette preferences, UTF-8 capability
    • Generates ANSI escape sequences for color output
  5. PACKET GENERATION:
    • Wraps ASCII frames in protocol packets with metadata
    • Includes checksums, dimensions, and capability flags
    • Queues packets for delivery via client send threads

PER-CLIENT CUSTOMIZATION:

Unlike traditional video mixing that generates one output, this system creates personalized frames for each client:

TERMINAL CAPABILITY AWARENESS:

  • Color depth: 1-bit (mono), 8-color, 16-color, 256-color, 24-bit RGB
  • Character support: ASCII-only vs UTF-8 box drawing
  • Render modes: foreground, background, half-block (2x resolution)
  • Custom ASCII palettes: brightness-to-character mapping

PERFORMANCE OPTIMIZATIONS:

  • Per-client palette caches (avoid repeated initialization)
  • Double-buffer system for smooth frame delivery
  • buffer domain allocation to reduce malloc/free overhead
  • Aggressive frame dropping under load
  • Zero-copy operations where possible

THREADING AND CONCURRENCY:

This module operates in a high-concurrency environment:

THREAD SAFETY MECHANISMS:

  • Double-buffer thread safety (atomic operations)
  • Reader-writer locks on client manager (allows concurrent reads)
  • buffer domain thread safety (lock-free where possible)
  • Atomic snapshot operations for client state

PERFORMANCE CHARACTERISTICS:

  • Supports 60fps per client with linear scaling
  • Handles burst traffic with frame buffer overruns
  • Minimal CPU overhead with double-buffer system
  • Memory usage scales with number of active clients

FRAME BUFFER MANAGEMENT:

DOUBLE-BUFFER STRATEGY: Each client uses a double-buffer system for smooth frame delivery:

  • Always provides the latest available frame
  • No frame caching complexity or stale data concerns
  • Professional-grade real-time performance like Zoom/Google Meet
  • Memory managed via owned buffers

BUFFER OVERFLOW HANDLING: When clients send frames faster than processing:

  • Aggressive frame dropping (keep only latest)
  • Logarithmic drop rate based on buffer occupancy
  • Maintains real-time performance under load

INTEGRATION WITH OTHER MODULES:

  • render.c: Calls frame generation functions at 60fps per client
  • client.c: Provides client state and capabilities
  • protocol.c: Stores incoming video frames in per-client buffers
  • video/: Performs actual RGB-to-ASCII conversion
  • palette.c: Manages client-specific character palettes

WHY THIS MODULAR DESIGN:

The original server.c contained all video mixing logic inline, making it:

  • Impossible to understand the video pipeline
  • Difficult to optimize performance bottlenecks
  • Hard to add new rendering features
  • Challenging to debug frame generation issues

This separation provides:

  • Clear video processing pipeline
  • Isolated performance optimization
  • Easier feature development
  • Better error isolation and debugging

MEMORY MANAGEMENT PHILOSOPHY:

  • All allocations use owned buffers where possible
  • Frame data is reference-counted (avoid copies)
  • Double-buffer system eliminates need for frame copies
  • Automatic cleanup on client disconnect
  • Graceful degradation on allocation failures
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
September 2025
Version
2.0 (Post-Modularization)
See also
render.c For frame generation scheduling
video/ For RGB-to-ASCII conversion implementation
palette.c For client Character Palettes management

Definition in file src/server/stream.c.

Macro Definition Documentation

◆ SERVER_STREAM_ASCII_BUFFER_SIZE

#define SERVER_STREAM_ASCII_BUFFER_SIZE   100

Definition at line 133 of file src/server/stream.c.

Referenced by create_mixed_ascii_frame_for_client().

◆ SERVER_STREAM_HEX_BUFFER_SIZE

#define SERVER_STREAM_HEX_BUFFER_SIZE   300

Definition at line 132 of file src/server/stream.c.

Referenced by create_mixed_ascii_frame_for_client().

Function Documentation

◆ any_clients_sending_video()

bool any_clients_sending_video ( void )

Queue ASCII frame for delivery to specific client.

Packages a generated ASCII frame into a protocol packet and queues it for asynchronous delivery to the target client. This function handles all protocol details including headers, checksums, and metadata.

PACKET STRUCTURE CREATED:

  • ascii_frame_packet_t header containing:
    • width, height: Client terminal dimensions
    • original_size: Uncompressed frame size
    • compressed_size: Compressed size (currently 0)
    • checksum: CRC32 of frame data for integrity
    • flags: Capability flags (color support, etc.)
  • Frame data: Complete ASCII art string with ANSI codes

PROTOCOL INTEGRATION:

  • Converts dimensions to network byte order
  • Generates CRC32 checksum for error detection
  • Sets appropriate capability flags based on client
  • Uses PACKET_TYPE_ASCII_FRAME packet type

DELIVERY MECHANISM:

  • Queues packet in client's video packet queue
  • Send thread delivers packet asynchronously
  • Queue overflow handling prevents memory exhaustion
  • Error conditions are logged but don't affect other clients

MEMORY MANAGEMENT:

  • Creates temporary packet buffer for header+data combination
  • packet_queue_push_copy() copies data (safe to free temp buffer)
  • No memory leaks on success or failure paths
  • buffer domain allocation not used here (temporary buffer)

ERROR HANDLING:

  • Invalid parameters: Return -1, no side effects
  • Memory allocation failure: Logged, return -1
  • Queue overflow: Logged as debug (expected under load)
  • Client shutdown: Graceful failure without error spam
Parameters
clientTarget client for frame delivery
ascii_frameGenerated ASCII art string (null-terminated)
frame_sizeSize of ASCII frame in bytes
Returns
0 on successful queuing, -1 on error
Note
Frame delivery happens asynchronously via send thread
Queue overflow drops frames to maintain real-time performance
Checksums enable client-side integrity verification
See also
packet_queue_push_copy() For underlying queue implementation
asciichat_crc32() For checksum generation

Check if any connected clients are currently sending video

This function scans all active clients to determine if at least one is sending video frames. Used by render threads to avoid generating frames when no video sources are available (e.g., during webcam warmup).

LOCK OPTIMIZATION: Uses atomic reads only, no rwlock acquisition This is safe because client_id, active, and is_sending_video are all atomics

Returns
true if at least one client has is_sending_video flag set, false otherwise

Definition at line 1283 of file src/server/stream.c.

1283 {
1284 // LOCK OPTIMIZATION: Don't acquire rwlock - all fields we access are atomic
1285 // client_id, active, is_sending_video are all atomic variables
1286
1287 // Iterate through all client slots
1288 for (int i = 0; i < MAX_CLIENTS; i++) {
1289 client_info_t *client = &g_client_manager.clients[i];
1290
1291 // Skip uninitialized clients (atomic read)
1292 if (client->client_id[0] == '\0') {
1293 continue;
1294 }
1295
1296 // Check if client is active and sending video (both atomic reads)
1297 bool is_active = atomic_load_bool(&client->active);
1298 bool is_sending = atomic_load_bool(&client->is_sending_video);
1299
1300 if (is_active && is_sending) {
1301 return true;
1302 }
1303 }
1304
1305 return false;
1306}
#define MAX_CLIENTS
Maximum possible clients (static array size) - actual runtime limit set by –max-clients (1-128).
struct client_info client_info_t
client_manager_t g_client_manager
Hash table entry for client packet dispatch.
char client_id[MAX_CLIENT_ID_LEN]

References client_info::active, client_info::client_id, g_client_manager, client_info::is_sending_video, and MAX_CLIENTS.

Referenced by client_video_render_thread().

◆ create_mixed_ascii_frame_for_client()

char * create_mixed_ascii_frame_for_client ( const char * target_client_id,
unsigned short width,
unsigned short height,
bool wants_stretch,
size_t * out_size,
bool * out_grid_changed,
int * out_sources_count,
acip_render_settings_t * out_rendering )

Generate personalized ASCII frame for a specific client.

This is the core video mixing function that creates customized ASCII art frames for individual clients. It collects video from all active clients, creates an appropriate grid layout, and converts to ASCII using the target client's terminal capabilities.

ALGORITHM OVERVIEW:

  1. FRAME COLLECTION PHASE:
    • Scan all active clients for available video frames
    • Use double-buffer system to get latest frames
    • Implement aggressive frame dropping under load (maintains real-time)
    • Always use the most current frame for professional-grade quality
  2. LAYOUT CALCULATION PHASE:
    • Determine grid dimensions based on active client count
    • Calculate cell sizes with aspect ratio preservation
    • Handle special cases: single client, multiple clients
  3. COMPOSITE GENERATION PHASE:
    • Create composite image with appropriate dimensions
    • Place each client's video in grid cell with proper scaling
    • Support both normal and half-block rendering modes
  4. ASCII CONVERSION PHASE:
    • Convert composite to ASCII using client capabilities
    • Apply client-specific palette and color settings
    • Generate ANSI escape sequences if supported

PERFORMANCE OPTIMIZATIONS:

FRAME BUFFER MANAGEMENT:

  • Aggressive frame dropping: always read latest available
  • Double-buffer system: ensures smooth frame delivery
  • buffer domain allocation: reduces malloc/free overhead
  • Zero-copy operations where possible

CONCURRENCY OPTIMIZATIONS:

  • Reader locks on client manager (allows parallel execution)
  • Double-buffer atomic operations (lock-free access)
  • Atomic snapshots of client state
  • Thread-safe buffer operations

CLIENT CAPABILITY INTEGRATION:

TERMINAL AWARENESS:

  • Respects client's color depth capabilities
  • Uses client's preferred ASCII palette
  • Handles UTF-8 vs ASCII-only terminals
  • Supports half-block mode for 2x vertical resolution

RENDERING MODES SUPPORTED:

  • RENDER_MODE_FOREGROUND: Traditional ASCII art
  • RENDER_MODE_BACKGROUND: Inverted colors for better contrast
  • RENDER_MODE_HALF_BLOCK: Unicode half-blocks for 2x resolution

ERROR HANDLING STRATEGY:

  • Invalid dimensions: Logged and frame generation aborted
  • Memory allocation failures: Graceful degradation, cleanup resources
  • Client disconnections: Skip client in current frame, continue
  • Buffer overruns: Frame dropping maintains real-time performance
  • Capability missing: Wait for capabilities before generating frames

MEMORY MANAGEMENT:

  • All image operations use buffer domain allocation
  • Frame data is properly freed after use
  • Cached frames use deep copies (prevent corruption)
  • Automatic cleanup on function exit
Parameters
target_client_idClient who will receive this customized frame
widthTerminal width in characters for this client
heightTerminal height in characters for this client
wants_stretchUnused parameter (aspect ratio always preserved)
out_sizePointer to store resulting ASCII frame size
Returns
Allocated ASCII frame string (caller must free), or NULL on error
Note
This function is called at 60fps per client by render threads
Generated frame is customized for target client's capabilities
Memory allocated from buffer domain - caller must free properly
Warning
Function performs extensive validation - invalid input returns NULL
Client capabilities must be received before frame generation
See also
ascii_convert_with_capabilities() For ASCII conversion implementation
framebuffer_read_multi_frame() For frame Owned Buffers operations
calculate_fit_dimensions_pixel() For aspect ratio calculations

Definition at line 1024 of file src/server/stream.c.

1026 {
1027 (void)wants_stretch; // Unused - we always handle aspect ratio ourselves
1028
1029 // Register the stream counter once even when per-client render workers enter
1030 // this function concurrently.
1031 static _Atomic bool stream_atomics_registered = false;
1032 if (!atomic_exchange_explicit(&stream_atomics_registered, true, memory_order_acq_rel)) {
1033 NAMED_REGISTER_ATOMIC(&g_previous_active_video_count, "server_video_source_count_for_layout_detection", NULL);
1034 }
1035
1036 uint64_t frame_gen_start_ns = time_get_ns();
1037
1038 // Initialize output parameters
1039 if (out_grid_changed) {
1040 *out_grid_changed = false;
1041 }
1042 if (out_sources_count) {
1043 *out_sources_count = 0;
1044 }
1045
1046 if (!out_size || width == 0 || height == 0) {
1047 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: width=%u, height=%u, out_size=%p", width, height, out_size);
1048 return NULL;
1049 }
1050
1051 // Collect all active clients and their image sources
1052 image_source_t sources[MAX_CLIENTS];
1053 uint64_t collect_start_ns = time_get_ns();
1054 int source_count = collect_video_sources(sources, MAX_CLIENTS);
1055 uint64_t collect_end_ns = time_get_ns();
1056
1057 // Count sources that actually have video data
1058 int sources_with_video = 0;
1059 for (int i = 0; i < source_count; i++) {
1060 if (sources[i].has_video && sources[i].image) {
1061 sources_with_video++;
1062 }
1063 }
1064
1065 log_dev_every(333 * NS_PER_MS_INT, "FRAME_GEN_START: target_client=%s sources=%d collect=%.1fms", target_client_id,
1066 sources_with_video, (collect_end_ns - collect_start_ns) / NS_PER_MS);
1067
1068 // Return the source count for debugging/tracking
1069 if (out_sources_count) {
1070 *out_sources_count = sources_with_video;
1071 }
1072
1073 // GRID LAYOUT CHANGE DETECTION:
1074 // Check if the number of active video sources has changed
1075 // NOTE: We only UPDATE the count and SIGNAL the change via out parameter
1076 // Broadcasting CLEAR_CONSOLE must happen AFTER the new frames are written to buffers
1077 // to prevent race condition where CLEAR arrives before new frame is ready
1078 uint64_t previous_count = atomic_load_u64(&g_previous_active_video_count);
1079 if (sources_with_video != (int)previous_count) {
1080 // Use compare-and-swap to ensure only ONE thread detects the change
1081 if (atomic_cas_u64(&g_previous_active_video_count, &previous_count, (uint64_t)sources_with_video)) {
1084 "Grid layout changing: %d -> %d active video sources - caller will broadcast clear AFTER buffering frame",
1085 previous_count, sources_with_video);
1086 if (out_grid_changed) {
1087 *out_grid_changed = true; // Signal to caller
1088 }
1089 }
1090 }
1091
1092 // No active video sources - don't generate placeholder frames
1093 image_t *composite = NULL;
1094
1095 if (sources_with_video == 0) {
1096 *out_size = 0;
1097 // No active video sources for client - this isn't an error.
1098 // Return NULL to indicate no frame should be sent
1099 return NULL;
1100 }
1101
1102 if (sources_with_video == 1) {
1103 // Single source handling - create composite and convert to ASCII
1104 // Note: create_single_source_composite returns a reference to sources[i].image
1105 // which could be modified by other threads. Make a copy to prevent concurrent
1106 // modification during ascii_convert_with_capabilities.
1107 image_t *single_source = create_single_source_composite(sources, source_count, target_client_id, width, height);
1108 if (single_source) {
1109 composite = image_new_copy(single_source);
1110 if (!composite) {
1111 SET_ERRNO(ERROR_MEMORY, "Failed to copy single source composite");
1112 *out_size = 0;
1113 return NULL;
1114 }
1115 }
1116 } else {
1117 // Multiple sources - create grid layout
1118 composite =
1119 create_multi_source_composite(sources, source_count, sources_with_video, target_client_id, width, height);
1120 }
1121
1122 char *out = NULL;
1123
1124 if (!composite) {
1125 SET_ERRNO(ERROR_INVALID_STATE, "Per-client %s: Failed to create composite image", target_client_id);
1126 *out_size = 0;
1127 return NULL;
1128 }
1129
1130 // Convert composite to ASCII using client capabilities
1131 // Pass terminal dimensions so the frame can be padded to full width
1132 char *ascii_frame = convert_composite_to_ascii(composite, target_client_id, width, height, out_rendering, sources,
1133 source_count, sources_with_video);
1134
1135 if (ascii_frame) {
1136 // The frame should have been null-terminated by the padding functions.
1137 // Use strlen() which is optimized and reliable
1138 size_t ascii_len = strlen(ascii_frame);
1139
1140 // Safety check: don't accept unreasonably large frames (10MB limit)
1141 if (ascii_len > 10 * 1024 * 1024) {
1142 log_error("Frame size exceeds 10MB safety limit (possible buffer overflow)");
1143 SET_ERRNO(ERROR_INVALID_PARAM, "Frame size exceeds 10MB");
1144 SAFE_FREE(ascii_frame);
1145 image_destroy(composite);
1146 *out_size = 0;
1147 return NULL;
1148 }
1149
1150 *out_size = ascii_len; // Renderer owns complete rows; never truncate to a color reset.
1151
1152 log_dev_every(LOG_RATE_SLOW, "Final frame size=%zu bytes for client %s", *out_size, target_client_id);
1153
1154 // Debug: Log the last 50 bytes of the frame to see what's really there
1155 if (*out_size >= 50) {
1156 char hex_buf[SERVER_STREAM_HEX_BUFFER_SIZE] = {0};
1157 size_t hex_len = 0;
1158 const uint8_t *last_bytes = (const uint8_t *)ascii_frame + (*out_size - 50);
1159 for (int i = 0; i < 50 && hex_len < sizeof(hex_buf) - 5; i++) {
1160 hex_len += snprintf(hex_buf + hex_len, sizeof(hex_buf) - hex_len, "%02X ", last_bytes[i]);
1161 }
1162 log_dev_every(4500 * US_PER_MS_INT, "FRAME_LAST_50_BYTES (hex): %s", hex_buf);
1163
1164 // Also log as ASCII for readability
1165 char ascii_buf[SERVER_STREAM_ASCII_BUFFER_SIZE] = {0};
1166 for (int i = 0; i < 50 && i < (int)sizeof(ascii_buf) - 1; i++) {
1167 if (last_bytes[i] >= 32 && last_bytes[i] < 127) {
1168 ascii_buf[i] = (char)last_bytes[i];
1169 } else if (last_bytes[i] == '\n') {
1170 ascii_buf[i] = 'N';
1171 } else if (last_bytes[i] == '\0') {
1172 ascii_buf[i] = '0';
1173 } else {
1174 ascii_buf[i] = '.';
1175 }
1176 }
1177 log_dev_every(4500 * US_PER_MS_INT, "FRAME_LAST_50_ASCII: %s", ascii_buf);
1178 }
1179
1180 out = ascii_frame;
1181 } else {
1182 SET_ERRNO(ERROR_TERMINAL, "Per-client %s: Failed to convert image to ASCII", target_client_id);
1183 *out_size = 0;
1184 }
1185
1186 if (composite) {
1187 image_destroy(composite);
1188 }
1189 for (int i = 0; i < source_count; i++) {
1190 if (sources[i].image) {
1191 image_destroy(sources[i].image);
1192 }
1193 }
1194
1195 uint64_t frame_gen_end_ns = time_get_ns();
1196 uint64_t frame_gen_duration_ns = frame_gen_end_ns - frame_gen_start_ns;
1197 if (frame_gen_duration_ns > 10 * NS_PER_MS_INT) { // Log if > 10ms
1198 char duration_str[TIME_DURATION_BUFFER_SIZE];
1199 time_pretty((uint64_t)((double)frame_gen_duration_ns), -1, duration_str, sizeof(duration_str));
1200 log_warn_every(LOG_RATE_DEFAULT, "SLOW_FRAME_GENERATION: Client %s full frame gen took %s", target_client_id,
1201 duration_str);
1202 }
1203
1204 return out;
1205}
#define SET_ERRNO(code,...)
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
#define SAFE_FREE(ptr)
Definition common.h:377
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t with automatic format specifier.
@ ERROR_INVALID_STATE
@ ERROR_MEMORY
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
@ ERROR_TERMINAL
Definition error_codes.h:64
#define LOG_RATE_SLOW
Log rate limit: 10 seconds (10,000,000 microseconds).
Definition log_rates.h:35
#define LOG_RATE_DEFAULT
Log rate limit: 5 seconds (5,000,000 microseconds) - default for audio/video packets.
Definition log_rates.h:32
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as a compact duration with configurable sub-minute precision.
Definition time.c:374
#define TIME_DURATION_BUFFER_SIZE
Definition time.h:40
#define NS_PER_MS_INT
Definition time.h:168
#define NS_PER_MS
Definition time.h:159
#define US_PER_MS_INT
Definition time.h:172
image_t * image_new_copy(const image_t *source)
Create a copy of an existing image.
void image_destroy(image_t *image)
Destroy an image allocated with image_new().
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:755
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:746
static bool size_t gotel_query_t * out
Definition queries.h:8
#define SERVER_STREAM_HEX_BUFFER_SIZE
#define SERVER_STREAM_ASCII_BUFFER_SIZE
One client's video input as seen by the compositor.
SIMD-friendly storage for one RGB pixel. r, g, and b hold the color channels and padding keeps the la...

References ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_MEMORY, ERROR_TERMINAL, height, image_destroy(), image_new_copy(), log_dev_every, log_error, LOG_RATE_DEFAULT, LOG_RATE_SLOW, log_warn_every, MAX_CLIENTS, NAMED_REGISTER_ATOMIC, NS_PER_MS, NS_PER_MS_INT, out, SAFE_FREE, SERVER_STREAM_ASCII_BUFFER_SIZE, SERVER_STREAM_HEX_BUFFER_SIZE, SET_ERRNO, TIME_DURATION_BUFFER_SIZE, time_pretty(), US_PER_MS_INT, and width.

Referenced by client_video_render_thread().