ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
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
 Image source structure for multi-client video mixing. More...
 

Functions

char * create_mixed_ascii_frame_for_client (uint32_t target_client_id, unsigned short width, unsigned short height, bool wants_stretch, size_t *out_size, bool *out_grid_changed, int *out_sources_count)
 Generate personalized ASCII frame for a specific client.
 
int queue_audio_for_client (client_info_t *client, const void *audio_data, size_t data_size)
 Queue ASCII frame for delivery to specific client.
 
bool any_clients_sending_video (void)
 Check if any connected clients are currently sending video.
 

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 buffer pools 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 pool 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 pool 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 buffer pools

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 buffer pools 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 stream.c.

Function Documentation

◆ any_clients_sending_video()

bool any_clients_sending_video ( void  )

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 1305 of file stream.c.

1305 {
1306 // LOCK OPTIMIZATION: Don't acquire rwlock - all fields we access are atomic
1307 // client_id, active, is_sending_video are all atomic variables
1308
1309 // Iterate through all client slots
1310 for (int i = 0; i < MAX_CLIENTS; i++) {
1311 client_info_t *client = &g_client_manager.clients[i];
1312
1313 // Skip uninitialized clients (atomic read)
1314 if (atomic_load(&client->client_id) == 0) {
1315 continue;
1316 }
1317
1318 // Check if client is active and sending video (both atomic reads)
1319 bool is_active = atomic_load(&client->active);
1320 bool is_sending = atomic_load(&client->is_sending_video);
1321
1322 if (is_active && is_sending) {
1323 return true;
1324 }
1325 }
1326
1327 return false;
1328}
client_manager_t g_client_manager
Global client manager singleton - central coordination point.
client_info_t clients[MAX_CLIENTS]
Array of client_info_t structures (backing storage)
Definition client.h:65

References client_manager_t::clients, and g_client_manager.

Referenced by client_video_render_thread().

◆ create_mixed_ascii_frame_for_client()

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

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 pool 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 pool 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 pool - 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 buffer operations
calculate_fit_dimensions_pixel() For aspect ratio calculations

Definition at line 955 of file stream.c.

957 {
958 (void)wants_stretch; // Unused - we always handle aspect ratio ourselves
959
960 uint64_t frame_gen_start_ns = time_get_ns();
961
962 // Initialize output parameters
963 if (out_grid_changed) {
964 *out_grid_changed = false;
965 }
966 if (out_sources_count) {
967 *out_sources_count = 0;
968 }
969
970 if (!out_size || width == 0 || height == 0) {
971 SET_ERRNO(ERROR_INVALID_PARAM,
972 "Invalid parameters for create_mixed_ascii_frame_for_client: width=%u, height=%u, out_size=%p", width,
973 height, out_size);
974 return NULL;
975 }
976
977 // Collect all active clients and their image sources
978 image_source_t sources[MAX_CLIENTS];
979 uint64_t collect_start_ns = time_get_ns();
980 int source_count = collect_video_sources(sources, MAX_CLIENTS);
981 uint64_t collect_end_ns = time_get_ns();
982
983 // Count sources that actually have video data
984 int sources_with_video = 0;
985 for (int i = 0; i < source_count; i++) {
986 if (sources[i].has_video && sources[i].image) {
987 sources_with_video++;
988 }
989 }
990
991 static uint64_t last_detailed_log = 0;
992 uint64_t now_ns = collect_end_ns;
993 if (now_ns - last_detailed_log > 333 * NS_PER_MS_INT) { // Log every 333ms (3x per second)
994 last_detailed_log = now_ns;
995 log_info("FRAME_GEN_START: target_client=%u sources=%d collect=%.1fms", target_client_id, sources_with_video,
996 (collect_end_ns - collect_start_ns) / NS_PER_MS);
997 }
998
999 // Return the source count for debugging/tracking
1000 if (out_sources_count) {
1001 *out_sources_count = sources_with_video;
1002 }
1003
1004 // GRID LAYOUT CHANGE DETECTION:
1005 // Check if the number of active video sources has changed
1006 // NOTE: We only UPDATE the count and SIGNAL the change via out parameter
1007 // Broadcasting CLEAR_CONSOLE must happen AFTER the new frames are written to buffers
1008 // to prevent race condition where CLEAR arrives before new frame is ready
1009 int previous_count = atomic_load(&g_previous_active_video_count);
1010 if (sources_with_video != previous_count) {
1011 // Use compare-and-swap to ensure only ONE thread detects the change
1012 if (atomic_compare_exchange_strong(&g_previous_active_video_count, &previous_count, sources_with_video)) {
1013 log_dev_every(
1014 LOG_RATE_DEFAULT,
1015 "Grid layout changing: %d -> %d active video sources - caller will broadcast clear AFTER buffering frame",
1016 previous_count, sources_with_video);
1017 if (out_grid_changed) {
1018 *out_grid_changed = true; // Signal to caller
1019 }
1020 }
1021 }
1022
1023 // No active video sources - don't generate placeholder frames
1024 image_t *composite = NULL;
1025
1026 if (sources_with_video == 0) {
1027 *out_size = 0;
1028 // No active video sources for client - this isn't an error.
1029 // Return NULL to indicate no frame should be sent
1030 return NULL;
1031 }
1032
1033 if (sources_with_video == 1) {
1034 // Single source handling - create composite and convert to ASCII
1035 // Note: create_single_source_composite returns a reference to sources[i].image
1036 // which could be modified by other threads. Make a copy to prevent concurrent
1037 // modification during ascii_convert_with_capabilities.
1038 image_t *single_source = create_single_source_composite(sources, source_count, target_client_id, width, height);
1039 if (single_source) {
1040 composite = image_new_copy(single_source);
1041 if (!composite) {
1042 SET_ERRNO(ERROR_MEMORY, "Failed to copy single source composite");
1043 *out_size = 0;
1044 return NULL;
1045 }
1046 }
1047 } else {
1048 // Multiple sources - create grid layout
1049 composite =
1050 create_multi_source_composite(sources, source_count, sources_with_video, target_client_id, width, height);
1051 }
1052
1053 char *out = NULL;
1054
1055 if (!composite) {
1056 SET_ERRNO(ERROR_INVALID_STATE, "Per-client %u: Failed to create composite image", target_client_id);
1057 *out_size = 0;
1058 out = NULL;
1059 }
1060
1061 // Convert composite to ASCII using client capabilities
1062 // Pass terminal dimensions so the frame can be padded to full width
1063 char *ascii_frame = convert_composite_to_ascii(composite, target_client_id, width, height);
1064
1065 if (ascii_frame) {
1066 // The frame should have been null-terminated by the padding functions.
1067 // Use strlen() which is optimized and reliable
1068 size_t ascii_len = strlen(ascii_frame);
1069
1070 // Safety check: don't accept unreasonably large frames (10MB limit)
1071 if (ascii_len > 10 * 1024 * 1024) {
1072 log_error("Frame size exceeds 10MB safety limit (possible buffer overflow)");
1073 SET_ERRNO(ERROR_INVALID_PARAM, "Frame size exceeds 10MB");
1074 return NULL;
1075 }
1076
1077 // Ensure frame ends with a reset sequence to avoid garbage at terminal
1078 // This prevents color codes from leaking into uninitialized memory
1079 const char reset_seq[] = "\033[0m";
1080 const size_t reset_len = 4;
1081
1082 if (ascii_len >= reset_len) {
1083 // Check if frame already ends with reset
1084 const char *frame_end = ascii_frame + ascii_len - reset_len;
1085 if (strncmp(frame_end, reset_seq, reset_len) == 0) {
1086 // Frame properly ends with reset, use full length
1087 *out_size = ascii_len;
1088 } else {
1089 // Frame doesn't end with reset - this is the REAL bug!
1090 // Truncate to the last occurrence of reset sequence
1091 const char *last_reset = NULL;
1092 for (const char *p = ascii_frame + ascii_len - reset_len; p >= ascii_frame; p--) {
1093 if (strncmp(p, reset_seq, reset_len) == 0) {
1094 last_reset = p;
1095 break;
1096 }
1097 }
1098
1099 if (last_reset) {
1100 // Include the reset sequence and truncate after it
1101 *out_size = (size_t)(last_reset - ascii_frame) + reset_len;
1102 ascii_frame[*out_size] = '\0'; // Ensure null termination
1103 log_warn("Frame was missing reset at end (had garbage), truncated from %zu to %zu bytes", ascii_len,
1104 *out_size);
1105 } else {
1106 // No reset found, use full length as fallback
1107 *out_size = ascii_len;
1108 log_warn("Frame has no reset sequences, sending full %zu bytes", ascii_len);
1109 }
1110 }
1111 } else {
1112 // Frame too short to have a reset, use as-is
1113 *out_size = ascii_len;
1114 }
1115
1116 log_dev_every(LOG_RATE_SLOW, "create_mixed_ascii_frame_for_client: Final frame size=%zu bytes for client %u",
1117 *out_size, target_client_id);
1118
1119 // Debug: Log the last 50 bytes of the frame to see what's really there
1120 if (*out_size >= 50) {
1121 char hex_buf[300] = {0};
1122 size_t hex_len = 0;
1123 const uint8_t *last_bytes = (const uint8_t *)ascii_frame + (*out_size - 50);
1124 for (int i = 0; i < 50 && hex_len < sizeof(hex_buf) - 5; i++) {
1125 hex_len += snprintf(hex_buf + hex_len, sizeof(hex_buf) - hex_len, "%02X ", last_bytes[i]);
1126 }
1127 log_dev_every(4500 * US_PER_MS_INT, "FRAME_LAST_50_BYTES (hex): %s", hex_buf);
1128
1129 // Also log as ASCII for readability
1130 char ascii_buf[100] = {0};
1131 for (int i = 0; i < 50 && i < (int)sizeof(ascii_buf) - 1; i++) {
1132 if (last_bytes[i] >= 32 && last_bytes[i] < 127) {
1133 ascii_buf[i] = (char)last_bytes[i];
1134 } else if (last_bytes[i] == '\n') {
1135 ascii_buf[i] = 'N';
1136 } else if (last_bytes[i] == '\0') {
1137 ascii_buf[i] = '0';
1138 } else {
1139 ascii_buf[i] = '.';
1140 }
1141 }
1142 log_dev_every(4500 * US_PER_MS_INT, "FRAME_LAST_50_ASCII: %s", ascii_buf);
1143 }
1144
1145 out = ascii_frame;
1146 } else {
1147 SET_ERRNO(ERROR_TERMINAL, "Per-client %u: Failed to convert image to ASCII", target_client_id);
1148 *out_size = 0;
1149 }
1150
1151 if (composite) {
1152 // For single source, composite is a malloc-allocated copy, not from pool
1153 // Check alloc method to determine correct destroy function
1154 if (composite->alloc_method == IMAGE_ALLOC_POOL) {
1155 image_destroy_to_pool(composite);
1156 } else {
1157 image_destroy(composite);
1158 }
1159 }
1160 for (int i = 0; i < source_count; i++) {
1161 if (sources[i].image) {
1162 image_destroy_to_pool(sources[i].image);
1163 }
1164 }
1165
1166 uint64_t frame_gen_end_ns = time_get_ns();
1167 uint64_t frame_gen_duration_ns = frame_gen_end_ns - frame_gen_start_ns;
1168 if (frame_gen_duration_ns > 10 * NS_PER_MS_INT) { // Log if > 10ms
1169 char duration_str[32];
1170 format_duration_ns((double)frame_gen_duration_ns, duration_str, sizeof(duration_str));
1171 log_warn("SLOW_FRAME_GENERATION: Client %u full frame gen took %s", target_client_id, duration_str);
1172 }
1173
1174 return out;
1175}
Image source structure for multi-client video mixing.
Definition stream.c:204
uint64_t time_get_ns(void)
Definition util/time.c:48
int format_duration_ns(double nanoseconds, char *buffer, size_t buffer_size)
Definition util/time.c:275
image_t * image_new_copy(const image_t *source)
void image_destroy_to_pool(image_t *image)
void image_destroy(image_t *p)
Definition video/image.c:85

References format_duration_ns(), image_destroy(), image_destroy_to_pool(), image_new_copy(), and time_get_ns().

Referenced by client_video_render_thread().

◆ queue_audio_for_client()

int queue_audio_for_client ( client_info_t *  client,
const void *  audio_data,
size_t  data_size 
)

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_enqueue() copies data (safe to free temp buffer)
  • No memory leaks on success or failure paths
  • Buffer pool 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_enqueue() For underlying queue implementation
asciichat_crc32() For checksum generation

Queue audio data for delivery to specific client

Queues mixed audio data for delivery to a specific client. This is a simple wrapper around the packet queue system for audio delivery.

AUDIO PIPELINE INTEGRATION:

  • Called by audio mixing threads after combining multiple client streams
  • Audio data is already in final format (float samples, mixed)
  • No additional processing required at this stage

PACKET DETAILS:

  • Uses PACKET_TYPE_AUDIO_BATCH packet type
  • Audio data is raw float samples bundled together
  • Batch format reduces packet overhead ~32x
  • Sample rate and format determined by audio system

DELIVERY CHARACTERISTICS:

  • Higher priority than video packets (lower latency)
  • Uses client's audio packet queue
  • Send thread prioritizes audio over video
  • Queue overflow drops oldest audio to maintain real-time

ERROR HANDLING:

  • Invalid parameters: Return -1 immediately
  • Queue overflow: Handled by packet queue (drops old data)
  • Client shutdown: Graceful failure
Parameters
clientTarget client for audio delivery
audio_dataMixed audio samples (float format)
data_sizeSize of audio data in bytes
Returns
0 on successful queuing, -1 on error
Note
Audio has priority over video in send thread
Real-time audio requires queue overflow handling
See also
packet_queue_enqueue() For underlying queue implementation
mixer.c For Audio mixing implementation

Definition at line 1285 of file stream.c.

1285 {
1286 if (!client || !client->audio_queue || !audio_data || data_size == 0) {
1287 return -1;
1288 }
1289
1290 return packet_queue_enqueue(client->audio_queue, PACKET_TYPE_AUDIO_BATCH, audio_data, data_size, 0, true);
1291}
int packet_queue_enqueue(packet_queue_t *queue, packet_type_t type, const void *data, size_t data_len, uint32_t client_id, bool copy_data)

References packet_queue_enqueue().