ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
protocol.c File Reference

📡 Server packet processor: client communication handling, protocol state management, and packet dispatch More...

Go to the source code of this file.

Macros

#define OPUS_DECODE_STATIC_MAX_SAMPLES   (32 * 960)
 

Functions

void disconnect_client_for_bad_data (client_info_t *client, const char *format,...)
 
void handle_client_join_packet (client_info_t *client, const void *data, size_t len)
 Process CLIENT_JOIN packet - client announces identity and capabilities.
 
void handle_protocol_version_packet (client_info_t *client, const void *data, size_t len)
 Process PROTOCOL_VERSION packet - validate protocol compatibility.
 
void handle_client_leave_packet (client_info_t *client, const void *data, size_t len)
 Process CLIENT_LEAVE packet - handle clean client disconnect.
 
void handle_stream_start_packet (client_info_t *client, const void *data, size_t len)
 Process STREAM_START packet - client requests to begin media transmission.
 
void handle_stream_stop_packet (client_info_t *client, const void *data, size_t len)
 Process STREAM_STOP packet - client requests to halt media transmission.
 
void handle_ping_packet (client_info_t *client, const void *data, size_t len)
 Handle PING packet - respond with PONG.
 
void handle_pong_packet (client_info_t *client, const void *data, size_t len)
 Handle PONG packet - client acknowledged our PING.
 
void handle_image_frame_packet (client_info_t *client, void *data, size_t len)
 Process IMAGE_FRAME packet - store client's video data for rendering.
 
void handle_audio_packet (client_info_t *client, const void *data, size_t len)
 Process AUDIO packet - store single audio sample batch (legacy format)
 
void handle_remote_log_packet_from_client (client_info_t *client, const void *data, size_t len)
 
void handle_audio_batch_packet (client_info_t *client, const void *data, size_t len)
 Process AUDIO_BATCH packet - store efficiently batched audio samples.
 
void handle_audio_opus_batch_packet (client_info_t *client, const void *data, size_t len)
 Process AUDIO_OPUS_BATCH packet - efficient Opus-encoded audio batch from client.
 
void handle_audio_opus_packet (client_info_t *client, const void *data, size_t len)
 Process AUDIO_OPUS packet - decode single Opus frame from client.
 
void handle_client_capabilities_packet (client_info_t *client, const void *data, size_t len)
 Process CLIENT_CAPABILITIES packet - configure client-specific rendering.
 
void handle_size_packet (client_info_t *client, const void *data, size_t len)
 Process terminal size update packet - handle client window resize.
 
int send_server_state_to_client (client_info_t *client)
 Send current server state to a specific client.
 
void broadcast_clear_console_to_all_clients (void)
 Signal all active clients to clear their displays before next video frame.
 

Detailed Description

📡 Server packet processor: client communication handling, protocol state management, and packet dispatch

CORE RESPONSIBILITIES

  1. Parse and validate incoming packets from clients
  2. Update client state based on received packet data
  3. Coordinate with other modules for media processing
  4. Generate appropriate server responses to client requests
  5. Maintain protocol compliance and packet format standards

PACKET PROCESSING ARCHITECTURE:

The protocol processing follows a clear pattern:

  1. PACKET RECEPTION (in client.c receive thread):
    • Receives raw packet data from socket
    • Validates packet header and CRC
    • Dispatches to appropriate handler function
  2. HANDLER FUNCTION (this module):
    • Validates packet payload structure
    • Updates client state with thread-safe patterns
    • Processes media data (stores in buffers)
    • Generates any necessary responses
  3. RESPONSE GENERATION (via packet queues):
    • Queues response packets for delivery
    • Uses client's outgoing packet queues
    • Send thread delivers responses asynchronously

SUPPORTED PACKET TYPES:

CLIENT LIFECYCLE:

  • PACKET_TYPE_CLIENT_JOIN: Initial client capabilities and identity
  • PACKET_TYPE_CLIENT_LEAVE: Clean disconnect notification
  • PACKET_TYPE_CLIENT_CAPABILITIES: Terminal capabilities and preferences

MEDIA STREAMING:

  • PACKET_TYPE_STREAM_START: Begin sending audio/video
  • PACKET_TYPE_STREAM_STOP: Stop sending audio/video
  • PACKET_TYPE_IMAGE_FRAME: Raw RGB video frame data
  • PACKET_TYPE_AUDIO: Single audio sample packet (legacy)
  • PACKET_TYPE_AUDIO_BATCH: Batched audio samples (efficient)

CONTROL PROTOCOL:

  • PACKET_TYPE_PING: Client keepalive request
  • PACKET_TYPE_PONG: Server keepalive response

THREAD SAFETY AND STATE MANAGEMENT:

CLIENT STATE SYNCHRONIZATION: All client state modifications use the snapshot pattern:

  1. Acquire client->client_state_mutex
  2. Update client state fields
  3. Release mutex immediately
  4. Process using local copies if needed

MEDIA BUFFER COORDINATION: Video frames: Stored in client->incoming_video_buffer (thread-safe) Audio samples: Stored in client->incoming_audio_buffer (lock-free) Both buffers are processed by render threads in render.c

PACKET VALIDATION STRATEGY: All handlers validate:

  • Packet size matches expected structure size
  • Client capabilities permit the operation
  • Buffer pointers are valid before access
  • Network byte order conversion where needed

ERROR HANDLING PHILOSOPHY:

  • Invalid packets are logged but don't disconnect clients
  • Buffer allocation failures are handled gracefully
  • Network errors during responses don't affect client state
  • Shutdown conditions are detected and avoid error spam

INTEGRATION WITH OTHER MODULES:

  • client.c: Called by receive threads, manages client lifecycle
  • render.c: Consumes video/audio data stored by handlers
  • stream.c: Uses client capabilities for frame generation
  • main.c: Provides global state (g_server_should_exit, etc.)

WHY THIS MODULAR DESIGN:

The original server.c mixed protocol handling with connection management and rendering logic, making it difficult to:

  • Add new packet types
  • Modify protocol behavior
  • Debug packet-specific issues
  • Test protocol compliance

This separation provides:

  • Clear protocol specification
  • Easier protocol evolution
  • Better error isolation
  • Improved testability
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
September 2025
Version
2.0 (Post-Modularization)
See also
client.c For client receive thread implementation
render.c For media buffer consumption
network.h For Packet Types structure definitions

Definition in file server/protocol.c.

Macro Definition Documentation

◆ OPUS_DECODE_STATIC_MAX_SAMPLES

#define OPUS_DECODE_STATIC_MAX_SAMPLES   (32 * 960)

Function Documentation

◆ broadcast_clear_console_to_all_clients()

void broadcast_clear_console_to_all_clients ( void  )

Signal all active clients to clear their displays before next video frame.

Sets the needs_display_clear flag for all currently connected and active clients. This is used when the grid layout changes (clients join/leave) to ensure all clients clear their displays before receiving frames with the new layout.

ARCHITECTURE:

  • Uses atomic flag (needs_display_clear) instead of packet queue
  • Send thread checks flag and sends CLEAR_CONSOLE before next video frame
  • Guarantees CLEAR_CONSOLE arrives before new grid layout frame

SYNCHRONIZATION:

  • Acquires g_client_manager_rwlock for reading
  • Uses atomic operations for flag setting (no mutex needed)
  • Thread-safe access to client list

USAGE SCENARIO:

  • Called when active video source count changes
  • Ensures all clients clear before new grid layout is sent
  • Prevents visual artifacts from old content
Note
This function iterates all clients but only flags active ones
Non-blocking - just sets atomic flags

Definition at line 1756 of file server/protocol.c.

1756 {
1757 SET_ERRNO(ERROR_INVALID_STATE, "broadcast_clear_console_to_all_clients() called - unexpected usage");
1758 log_warn("CLEAR_CONSOLE is now sent from render threads, not broadcast");
1759}

◆ disconnect_client_for_bad_data()

void disconnect_client_for_bad_data ( client_info_t *  client,
const char *  format,
  ... 
)

Definition at line 152 of file server/protocol.c.

152 {
153 if (!client) {
154 return;
155 }
156
157 protocol_cleanup_thread_locals();
158
159 bool already_requested = atomic_exchange(&client->protocol_disconnect_requested, true);
160 if (already_requested) {
161 return;
162 }
163
164 char reason[BUFFER_SIZE_SMALL] = {0};
165 if (format) {
166 va_list args;
167 va_start(args, format);
168 safe_vsnprintf(reason, sizeof(reason), format, args);
169 va_end(args);
170 } else {
171 SAFE_STRNCPY(reason, "Protocol violation", sizeof(reason));
172 }
173
174 const char *reason_str = reason[0] != '\0' ? reason : "Protocol violation";
175 uint32_t client_id = atomic_load(&client->client_id);
176
177 socket_t socket_snapshot = INVALID_SOCKET_VALUE;
178 const crypto_context_t *crypto_ctx = NULL;
179 acip_transport_t *transport_snapshot = NULL;
180
181 mutex_lock(&client->client_state_mutex);
182 if (client->socket != INVALID_SOCKET_VALUE) {
183 socket_snapshot = client->socket;
184 if (client->crypto_initialized) {
185 crypto_ctx = crypto_handshake_get_context(&client->crypto_handshake_ctx);
186 }
187 }
188 mutex_unlock(&client->client_state_mutex);
189
190 // Get transport reference for WebSocket clients
191 mutex_lock(&client->send_mutex);
192 transport_snapshot = client->transport;
193 mutex_unlock(&client->send_mutex);
194
195 // NOTE: Disconnecting a client due to the client's own bad behavior isn't an
196 // error for us, it's desired behavior for us, so we simply warn and do not
197 // have a need for asciichat_errno here.
198 log_warn("Disconnecting client %u due to protocol violation: %s", client_id, reason_str);
199
200 if (socket_snapshot != INVALID_SOCKET_VALUE) {
201 // Protect socket writes with send_mutex to prevent race with send_thread.
202 // This receive_thread and send_thread both write to same socket.
203 mutex_lock(&client->send_mutex);
204
205 asciichat_error_t log_result =
206 log_network_message(socket_snapshot, (const struct crypto_context_t *)crypto_ctx, LOG_ERROR,
207 REMOTE_LOG_DIRECTION_SERVER_TO_CLIENT, "Protocol violation: %s", reason_str);
208 if (log_result != ASCIICHAT_OK) {
209 log_warn("Failed to send remote log to client %u: %s", client_id, asciichat_error_string(log_result));
210 }
211
212 asciichat_error_t send_result = packet_send_error(socket_snapshot, crypto_ctx, ERROR_NETWORK_PROTOCOL, reason_str);
213 if (send_result != ASCIICHAT_OK) {
214 log_warn("Failed to send error packet to client %u: %s", client_id, asciichat_error_string(send_result));
215 }
216
217 mutex_unlock(&client->send_mutex);
218 } else if (transport_snapshot) {
219 // For WebSocket clients, try to send error via transport
220 log_debug("Sending error to WebSocket client %u via transport", client_id);
221 acip_send_error(transport_snapshot, ERROR_NETWORK_PROTOCOL, reason_str);
222 }
223
225
226 log_debug("Setting active=false in disconnect_client_for_bad_data (client_id=%u, reason=%s)", client_id, reason_str);
227 atomic_store(&client->active, false);
228 atomic_store(&client->shutting_down, true);
229 atomic_store(&client->send_thread_running, false);
230 atomic_store(&client->video_render_thread_running, false);
231 atomic_store(&client->audio_render_thread_running, false);
232
233 if (client->audio_queue) {
234 packet_queue_stop(client->audio_queue);
235 }
236
237 mutex_lock(&client->client_state_mutex);
238 if (client->socket != INVALID_SOCKET_VALUE) {
239 socket_shutdown(client->socket, 2);
240 socket_close(client->socket);
241 client->socket = INVALID_SOCKET_VALUE;
242 }
243 mutex_unlock(&client->client_state_mutex);
244}
const crypto_context_t * crypto_handshake_get_context(const crypto_handshake_context_t *ctx)
int socket_t
asciichat_error_t log_network_message(socket_t sockfd, const struct crypto_context_t *crypto_ctx, log_level_t level, remote_log_direction_t direction, const char *fmt,...)
action_args_t args
asciichat_error_t packet_send_error(socket_t sockfd, const crypto_context_t *crypto_ctx, asciichat_error_t error_code, const char *message)
Definition packet.c:804
void packet_queue_stop(packet_queue_t *queue)
void platform_sleep_ms(unsigned int ms)
asciichat_error_t acip_send_error(acip_transport_t *transport, uint32_t error_code, const char *message)
Definition send.c:233
int safe_vsnprintf(char *buffer, size_t buffer_size, const char *format, va_list ap)
Safe formatted string printing with va_list.
Definition system.c:507

References acip_send_error(), args, crypto_handshake_get_context(), log_network_message(), packet_queue_stop(), packet_send_error(), platform_sleep_ms(), and safe_vsnprintf().

Referenced by handle_audio_batch_packet(), handle_audio_opus_batch_packet(), handle_audio_opus_packet(), handle_client_join_packet(), handle_image_frame_packet(), handle_protocol_version_packet(), handle_remote_log_packet_from_client(), and process_decrypted_packet().

◆ handle_audio_batch_packet()

void handle_audio_batch_packet ( client_info_t *  client,
const void *  data,
size_t  len 
)

Process AUDIO_BATCH packet - store efficiently batched audio samples.

Handles the optimized audio packet format that bundles multiple sample chunks into a single packet. This reduces packet overhead and improves network efficiency for audio streaming.

PACKET STRUCTURE EXPECTED:

  • audio_batch_packet_t header:
    • uint32_t batch_count: Number of sample chunks in this batch
    • uint32_t total_samples: Total number of float samples
    • uint32_t sample_rate: Samples per second (typically 44100)
    • uint32_t channels: Number of audio channels (1 = mono)
  • float samples[total_samples]: IEEE 754 sample data

PERFORMANCE ADVANTAGES:

  • Reduces packet count by 5-10x compared to single audio packets
  • Lower network overhead and CPU context switching
  • Better burst tolerance with larger buffers

VALIDATION PERFORMED:

  • Header size matches audio_batch_packet_t
  • Total packet size matches header + samples
  • Sample count is within reasonable bounds
  • Client is authorized to send audio

BUFFER MANAGEMENT:

  • Extracts samples from packet payload
  • Stores in client->incoming_audio_buffer (same as single format)
  • Ring buffer automatically handles overflow
  • mixer.c consumes batched samples identically

ERROR HANDLING:

  • Invalid batch headers are logged and packet dropped
  • Oversized batches are rejected (prevents DoS)
  • Buffer allocation failures are handled gracefully
Parameters
clientSource client providing batched audio data
dataPacket payload containing batch header and samples
lenTotal size of packet payload in bytes
Note
This is the preferred audio packet format
Batching is transparent to audio processing pipeline
See also
handle_audio_packet() For legacy single-batch format
AUDIO_BATCH_SAMPLES For maximum batch size constant

Definition at line 1040 of file server/protocol.c.

1040 {
1041 // Log every audio batch packet reception
1042 log_debug_every(LOG_RATE_DEFAULT, "Received audio batch packet from client %u (len=%zu, is_sending_audio=%d)",
1043 atomic_load(&client->client_id), len, atomic_load(&client->is_sending_audio));
1044
1045 VALIDATE_NOTNULL_DATA(client, data, "AUDIO_BATCH");
1046 VALIDATE_MIN_SIZE(client, len, sizeof(audio_batch_packet_t), "AUDIO_BATCH");
1047 VALIDATE_AUDIO_STREAM_ENABLED(client, "AUDIO_BATCH");
1048
1049 // Parse batch header using utility function
1050 audio_batch_info_t batch_info;
1051 asciichat_error_t parse_result = audio_parse_batch_header(data, len, &batch_info);
1052 if (parse_result != ASCIICHAT_OK) {
1053 disconnect_client_for_bad_data(client, "Failed to parse audio batch header");
1054 return;
1055 }
1056
1057 uint32_t packet_batch_count = batch_info.batch_count;
1058 uint32_t total_samples = batch_info.total_samples;
1059 uint32_t sample_rate = batch_info.sample_rate;
1060
1061 (void)packet_batch_count;
1062 (void)sample_rate;
1063
1064 VALIDATE_NONZERO(client, packet_batch_count, "batch_count", "AUDIO_BATCH");
1065 VALIDATE_NONZERO(client, total_samples, "total_samples", "AUDIO_BATCH");
1066
1067 size_t samples_bytes = 0;
1068 if (safe_size_mul(total_samples, sizeof(uint32_t), &samples_bytes)) {
1069 disconnect_client_for_bad_data(client, "AUDIO_BATCH sample size overflow (samples=%u)", total_samples);
1070 return;
1071 }
1072
1073 size_t expected_size = sizeof(audio_batch_packet_t) + samples_bytes;
1074 if (len != expected_size) {
1075 disconnect_client_for_bad_data(client, "AUDIO_BATCH length mismatch: got %zu expected %zu", len, expected_size);
1076 return;
1077 }
1078
1079 // Bounds check to prevent integer overflow on allocation
1080 // Maximum allowed samples: AUDIO_BATCH_SAMPLES * 2 (2048 samples)
1081 // This prevents total_samples * sizeof(float) from exceeding 8KB
1082 const uint32_t MAX_AUDIO_SAMPLES = AUDIO_BATCH_SAMPLES * 2;
1083 if (total_samples > MAX_AUDIO_SAMPLES) {
1084 disconnect_client_for_bad_data(client, "AUDIO_BATCH too many samples: %u (max: %u)", total_samples,
1085 MAX_AUDIO_SAMPLES);
1086 return;
1087 }
1088
1089 const uint8_t *samples_ptr = (const uint8_t *)data + sizeof(audio_batch_packet_t);
1090
1091 // Safe allocation: total_samples is bounded above, so multiplication won't overflow
1092 size_t alloc_size = (size_t)total_samples * sizeof(float);
1093 float *samples = SAFE_MALLOC(alloc_size, float *);
1094 if (!samples) {
1095 SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for audio sample conversion");
1096 return;
1097 }
1098
1099 // Use helper function to dequantize samples
1100 asciichat_error_t dq_result = audio_dequantize_samples(samples_ptr, total_samples, samples);
1101 if (dq_result != ASCIICHAT_OK) {
1102 SAFE_FREE(samples);
1103 return;
1104 }
1105
1106#ifndef NDEBUG
1107 static int recv_count = 0;
1108 recv_count++;
1109 if (recv_count % 100 == 0) {
1110 uint32_t raw0 = bytes_read_u32_unaligned(samples_ptr + 0 * sizeof(uint32_t));
1111 uint32_t raw1 = bytes_read_u32_unaligned(samples_ptr + 1 * sizeof(uint32_t));
1112 uint32_t raw2 = bytes_read_u32_unaligned(samples_ptr + 2 * sizeof(uint32_t));
1113 int32_t scaled0 = (int32_t)NET_TO_HOST_U32(raw0);
1114 int32_t scaled1 = (int32_t)NET_TO_HOST_U32(raw1);
1115 int32_t scaled2 = (int32_t)NET_TO_HOST_U32(raw2);
1116 log_info("RECV: network[0]=0x%08x, network[1]=0x%08x, network[2]=0x%08x", raw0, raw1, raw2);
1117 log_info("RECV: scaled[0]=%d, scaled[1]=%d, scaled[2]=%d", scaled0, scaled1, scaled2);
1118 log_info("RECV: samples[0]=%.6f, samples[1]=%.6f, samples[2]=%.6f", samples[0], samples[1], samples[2]);
1119 }
1120#endif
1121
1122 if (client->incoming_audio_buffer) {
1123 asciichat_error_t write_result = audio_ring_buffer_write(client->incoming_audio_buffer, samples, total_samples);
1124 if (write_result != ASCIICHAT_OK) {
1125 log_error("Failed to write decoded audio batch to buffer: %s", asciichat_error_string(write_result));
1126 }
1127 }
1128
1129 SAFE_FREE(samples);
1130}
asciichat_error_t audio_ring_buffer_write(audio_ring_buffer_t *rb, const float *data, int samples)
asciichat_error_t audio_parse_batch_header(const void *data, size_t len, audio_batch_info_t *out_batch)
asciichat_error_t audio_dequantize_samples(const uint8_t *samples_ptr, uint32_t total_samples, float *out_samples)
void disconnect_client_for_bad_data(client_info_t *client, const char *format,...)

References audio_dequantize_samples(), audio_parse_batch_header(), audio_ring_buffer_write(), audio_batch_info_t::batch_count, disconnect_client_for_bad_data(), audio_batch_info_t::sample_rate, and audio_batch_info_t::total_samples.

◆ handle_audio_packet()

void handle_audio_packet ( client_info_t *  client,
const void *  data,
size_t  len 
)

Process AUDIO packet - store single audio sample batch (legacy format)

Handles the original audio packet format that sends one batch of float samples per packet. This format is less efficient than AUDIO_BATCH but still supported for backward compatibility.

PACKET STRUCTURE:

  • float samples[len/sizeof(float)] (IEEE 754 format)
  • Sample rate assumed to be 44100 Hz
  • Mono audio (single channel)

PERFORMANCE CHARACTERISTICS:

BUFFER MANAGEMENT:

  • Stores samples in client->incoming_audio_buffer (lock-free ring buffer)
  • Buffer overflow drops oldest samples to maintain real-time behavior
  • mixer.c consumes samples for multi-client audio mixing

STATE VALIDATION:

  • Only processes if client->is_sending_audio is true
  • Requires valid buffer pointer and non-zero length
  • Handles buffer pointer safely during shutdown

ERROR HANDLING:

  • Invalid packets are silently ignored
  • Buffer overflow is handled by ring buffer (drops old data)
  • Graceful shutdown behavior
Parameters
clientSource client providing audio data
dataPacket payload containing float audio samples
lenSize of packet payload in bytes
Note
Prefer AUDIO_BATCH format for better efficiency
Audio processing happens in mixer threads, not here
See also
handle_audio_batch_packet() For efficient batched format
audio_ring_buffer_write() For storage implementation

Definition at line 943 of file server/protocol.c.

943 {
944 VALIDATE_NOTNULL_DATA(client, data, "AUDIO");
945 VALIDATE_AUDIO_ALIGNMENT(client, len, sizeof(float), "AUDIO");
946 VALIDATE_AUDIO_STREAM_ENABLED(client, "AUDIO");
947
948 int num_samples = (int)(len / sizeof(float));
949 VALIDATE_AUDIO_SAMPLE_COUNT(client, num_samples, AUDIO_SAMPLES_PER_PACKET, "AUDIO");
950 VALIDATE_RESOURCE_INITIALIZED(client, client->incoming_audio_buffer, "audio buffer");
951
952 const float *samples = (const float *)data;
953 asciichat_error_t result = audio_ring_buffer_write(client->incoming_audio_buffer, samples, num_samples);
954 if (result != ASCIICHAT_OK) {
955 log_error("Failed to write audio samples to buffer: %s", asciichat_error_string(result));
956 }
957}

References audio_ring_buffer_write().

◆ handle_client_capabilities_packet()

void handle_client_capabilities_packet ( client_info_t *  client,
const void *  data,
size_t  len 
)

Process CLIENT_CAPABILITIES packet - configure client-specific rendering.

This packet contains detailed information about the client's terminal capabilities and preferences. The server uses this data to generate appropriately formatted ASCII art and ANSI escape sequences.

PACKET STRUCTURE EXPECTED:

  • terminal_capabilities_packet_t containing:
    • width, height: Terminal dimensions in characters
    • capabilities: Bitmask of terminal features
    • color_level: ANSI color support level (1, 8, 16, 256, 24-bit)
    • color_count: Number of supported colors
    • render_mode: Foreground, background, or half-block rendering
    • term_type: $TERM environment variable value
    • colorterm: $COLORTERM environment variable value
    • utf8_support: Whether terminal supports UTF-8
    • palette_type: ASCII character palette preference
    • palette_custom: Custom character set if PALETTE_CUSTOM

STATE CHANGES PERFORMED:

  • Updates client dimensions (width, height)
  • Stores complete terminal capabilities structure
  • Initializes per-client ASCII palette cache
  • Sets client->has_terminal_caps = true

PALETTE INITIALIZATION: The function performs critical palette setup:

  1. Determines character set based on palette_type
  2. Handles custom palettes if provided
  3. Generates luminance-to-character mapping
  4. Caches results for fast ASCII generation

THREAD SAFETY:

  • All client state updates are mutex-protected
  • Uses client->client_state_mutex for atomicity
  • Safe to call concurrently with render threads

VALIDATION PERFORMED:

  • Packet size matches expected structure
  • String fields are safely copied with bounds checking
  • Palette initialization is verified
  • Network byte order conversion

ERROR HANDLING:

  • Invalid packets are logged and ignored
  • Palette initialization failures use server defaults
  • Missing capabilities default to safe values

INTEGRATION IMPACT:

  • render.c uses capabilities for ASCII generation
  • Color output depends on client's color_level
  • Character selection uses initialized palette
Parameters
clientTarget client whose capabilities are being configured
dataPacket payload containing terminal_capabilities_packet_t
lenSize of packet payload in bytes
Note
This packet is typically sent once after CLIENT_JOIN
Capabilities can be updated during the session
Changes affect all subsequent ASCII frame generation
See also
initialize_client_palette() For Character Palettes setup details
terminal_color_level_name() For color level descriptions

Definition at line 1475 of file server/protocol.c.

1475 {
1476 log_debug("CLIENT_CAPABILITIES: client_id=%u, data=%p, len=%zu", atomic_load(&client->client_id), data, len);
1477
1478 VALIDATE_PACKET_SIZE(client, data, len, sizeof(terminal_capabilities_packet_t), "CLIENT_CAPABILITIES");
1479
1480 const terminal_capabilities_packet_t *caps = (const terminal_capabilities_packet_t *)data;
1481
1482 // Extract and validate dimensions
1483 uint16_t width = NET_TO_HOST_U16(caps->width);
1484 uint16_t height = NET_TO_HOST_U16(caps->height);
1485 log_error("CLIENT_CAPABILITIES: dimensions=%ux%u", width, height);
1486
1487 VALIDATE_NONZERO(client, width, "width", "CLIENT_CAPABILITIES");
1488 VALIDATE_NONZERO(client, height, "height", "CLIENT_CAPABILITIES");
1489 VALIDATE_RANGE(client, width, 1, 4096, "width", "CLIENT_CAPABILITIES");
1490 VALIDATE_RANGE(client, height, 1, 4096, "height", "CLIENT_CAPABILITIES");
1491
1492 // Extract and validate color level (0=none, 1=16, 2=256, 3=truecolor)
1493 uint32_t color_level = NET_TO_HOST_U32(caps->color_level);
1494 VALIDATE_RANGE(client, color_level, 0, 3, "color_level", "CLIENT_CAPABILITIES");
1495
1496 // Extract and validate render mode (0=foreground, 1=background, 2=half-block)
1497 uint32_t render_mode = NET_TO_HOST_U32(caps->render_mode);
1498 VALIDATE_RANGE(client, render_mode, 0, 2, "render_mode", "CLIENT_CAPABILITIES");
1499
1500 // Extract and validate palette type (0-5 are valid, 5=PALETTE_CUSTOM)
1501 uint32_t palette_type = NET_TO_HOST_U32(caps->palette_type);
1502 VALIDATE_RANGE(client, palette_type, 0, 5, "palette_type", "CLIENT_CAPABILITIES");
1503
1504 // Validate desired FPS (1-144)
1505 VALIDATE_RANGE(client, caps->desired_fps, 1, 144, "desired_fps", "CLIENT_CAPABILITIES");
1506
1507 mutex_lock(&client->client_state_mutex);
1508
1509 atomic_store(&client->width, width);
1510 atomic_store(&client->height, height);
1511
1512 log_debug("Client %u dimensions: %ux%u, desired_fps=%u", atomic_load(&client->client_id), client->width,
1513 client->height, caps->desired_fps);
1514
1515 client->terminal_caps.capabilities = NET_TO_HOST_U32(caps->capabilities);
1516 client->terminal_caps.color_level = color_level;
1517 client->terminal_caps.color_count = NET_TO_HOST_U32(caps->color_count);
1518 client->terminal_caps.render_mode = render_mode;
1519 client->terminal_caps.detection_reliable = caps->detection_reliable;
1520 client->terminal_caps.wants_background = (render_mode == RENDER_MODE_BACKGROUND);
1521
1522 SAFE_STRNCPY(client->terminal_caps.term_type, caps->term_type, sizeof(client->terminal_caps.term_type));
1523 SAFE_STRNCPY(client->terminal_caps.colorterm, caps->colorterm, sizeof(client->terminal_caps.colorterm));
1524
1525 client->terminal_caps.utf8_support = NET_TO_HOST_U32(caps->utf8_support);
1526 client->terminal_caps.palette_type = palette_type;
1527 SAFE_STRNCPY(client->terminal_caps.palette_custom, caps->palette_custom,
1528 sizeof(client->terminal_caps.palette_custom));
1529
1530 client->terminal_caps.desired_fps = caps->desired_fps;
1531
1532 // Extract wants_padding flag (1=padding enabled, 0=no padding for snapshot/piped modes)
1533 client->terminal_caps.wants_padding = (caps->wants_padding != 0);
1534
1535 const char *custom_chars =
1536 (client->terminal_caps.palette_type == PALETTE_CUSTOM && client->terminal_caps.palette_custom[0])
1537 ? client->terminal_caps.palette_custom
1538 : NULL;
1539
1540 if (initialize_client_palette((palette_type_t)client->terminal_caps.palette_type, custom_chars,
1541 client->client_palette_chars, &client->client_palette_len,
1542 client->client_luminance_palette) == 0) {
1543 client->client_palette_type = (palette_type_t)client->terminal_caps.palette_type;
1544 client->client_palette_initialized = true;
1545 log_info("Client %d palette initialized: type=%u, %zu chars, utf8=%u", atomic_load(&client->client_id),
1546 client->terminal_caps.palette_type, client->client_palette_len, client->terminal_caps.utf8_support);
1547 } else {
1548 SET_ERRNO(ERROR_INVALID_STATE, "Failed to initialize palette for client %d", atomic_load(&client->client_id));
1549 client->client_palette_initialized = false;
1550 }
1551
1552 client->has_terminal_caps = true;
1553
1554 log_info("Client %u capabilities: %ux%u, color_level=%s (%u colors), caps=0x%x, term=%s, colorterm=%s, "
1555 "render_mode=%s, reliable=%s, fps=%u, wants_padding=%d",
1556 atomic_load(&client->client_id), client->width, client->height,
1557 terminal_color_level_name(client->terminal_caps.color_level), client->terminal_caps.color_count,
1558 client->terminal_caps.capabilities, client->terminal_caps.term_type, client->terminal_caps.colorterm,
1559 (client->terminal_caps.render_mode == RENDER_MODE_HALF_BLOCK
1560 ? "half-block"
1561 : (client->terminal_caps.render_mode == RENDER_MODE_BACKGROUND ? "background" : "foreground")),
1562 client->terminal_caps.detection_reliable ? "yes" : "no", client->terminal_caps.desired_fps,
1563 client->terminal_caps.wants_padding);
1564
1565 // Send capabilities acknowledgment to client
1566 if (client->socket != INVALID_SOCKET_VALUE) {
1567 log_info_client(client, "Terminal configured: %ux%u, %s, %s mode, %u fps", client->width, client->height,
1568 terminal_color_level_name(client->terminal_caps.color_level),
1569 (client->terminal_caps.render_mode == RENDER_MODE_HALF_BLOCK
1570 ? "half-block"
1571 : (client->terminal_caps.render_mode == RENDER_MODE_BACKGROUND ? "background" : "foreground")),
1572 client->terminal_caps.desired_fps);
1573 }
1574
1575 mutex_unlock(&client->client_state_mutex);
1576}
int initialize_client_palette(palette_type_t palette_type, const char *custom_chars, char client_palette_chars[256], size_t *client_palette_len, char client_luminance_palette[256])
Definition palette.c:299

References initialize_client_palette().

Referenced by add_client().

◆ handle_client_join_packet()

void handle_client_join_packet ( client_info_t *  client,
const void *  data,
size_t  len 
)

Process CLIENT_JOIN packet - client announces identity and capabilities.

This is the first substantive packet clients send after establishing a TCP connection. It provides the server with essential information for managing the client throughout its session.

PACKET STRUCTURE EXPECTED:

  • client_info_packet_t containing:
    • display_name: Human-readable client identifier
    • capabilities: Bitmask of CLIENT_CAP_* flags

STATE CHANGES PERFORMED:

  • Updates client->display_name from packet
  • Sets client->can_send_video based on CLIENT_CAP_VIDEO
  • Sets client->can_send_audio based on CLIENT_CAP_AUDIO
  • Sets client->wants_stretch based on CLIENT_CAP_STRETCH

PROTOCOL BEHAVIOR:

  • Does NOT automatically start media streams (requires STREAM_START)
  • Does NOT send CLEAR_CONSOLE to other clients (prevents flicker)
  • Logs client capabilities for debugging

ERROR HANDLING:

  • Silently ignores packets with wrong size
  • Invalid display names are truncated safely
  • Missing capabilities default to false
Parameters
clientTarget client whose state will be updated
dataPacket payload (should be client_info_packet_t)
lenSize of packet payload in bytes
Note
This function should only be called by client receive threads
Client state is already protected by receive thread serialization
See also
handle_stream_start_packet() For enabling media transmission

Definition at line 288 of file server/protocol.c.

288 {
289 VALIDATE_PACKET_SIZE(client, data, len, sizeof(client_info_packet_t), "CLIENT_JOIN");
290
291 const client_info_packet_t *join_info = (const client_info_packet_t *)data;
292
293 // Validate display name is present and not just whitespace
294 if (join_info->display_name[0] == '\0') {
295 disconnect_client_for_bad_data(client, "CLIENT_JOIN display_name cannot be empty");
296 return;
297 }
298
299 uint32_t capabilities = NET_TO_HOST_U32(join_info->capabilities);
300
301 // Validate at least one capability flag is set
302 const uint32_t VALID_CAP_MASK = CLIENT_CAP_VIDEO | CLIENT_CAP_AUDIO | CLIENT_CAP_COLOR | CLIENT_CAP_STRETCH;
303 VALIDATE_CAPABILITY_FLAGS(client, capabilities, VALID_CAP_MASK, "CLIENT_JOIN");
304
305 // Validate no unknown capability bits are set
306 VALIDATE_FLAGS_MASK(client, capabilities, VALID_CAP_MASK, "CLIENT_JOIN");
307
308 SAFE_STRNCPY(client->display_name, join_info->display_name, MAX_DISPLAY_NAME_LEN - 1);
309
310 client->can_send_video = (capabilities & CLIENT_CAP_VIDEO) != 0;
311 client->can_send_audio = (capabilities & CLIENT_CAP_AUDIO) != 0;
312 client->wants_stretch = (capabilities & CLIENT_CAP_STRETCH) != 0;
313
314 log_info("Client %u joined: %s (video=%d, audio=%d, stretch=%d)", atomic_load(&client->client_id),
315 client->display_name, client->can_send_video, client->can_send_audio, client->wants_stretch);
316
317 // Notify client of successful join (encrypted channel)
318 if (client->socket != INVALID_SOCKET_VALUE) {
319 log_info_client(client, "Joined as '%s' (video=%s, audio=%s)", client->display_name,
320 client->can_send_video ? "yes" : "no", client->can_send_audio ? "yes" : "no");
321 }
322}

References disconnect_client_for_bad_data().

◆ handle_client_leave_packet()

void handle_client_leave_packet ( client_info_t *  client,
const void *  data,
size_t  len 
)

Process CLIENT_LEAVE packet - handle clean client disconnect.

Clients may send this packet before disconnecting to allow the server to log the disconnect reason and perform clean state management. This is optional but preferred over abrupt disconnects.

PACKET STRUCTURE EXPECTED:

  • Optional string containing disconnect reason (0-256 bytes)

PROTOCOL BEHAVIOR:

  • Client logs the disconnect reason if provided
  • Server continues normal disconnect sequence after receiving packet
  • Client remains responsible for closing socket

ERROR HANDLING:

  • Empty payload is handled gracefully
  • Oversized payloads are rejected
  • Invalid UTF-8 in reason is handled gracefully (logged as-is)
Parameters
clientClient that sent the leave packet
dataPacket payload (optional reason string)
lenSize of packet payload in bytes (0-256)
Note
This handler doesn't trigger immediate disconnect
Actual disconnect occurs when socket closes

Definition at line 432 of file server/protocol.c.

432 {
433 if (!client) {
434 return;
435 }
436
437 uint32_t client_id = atomic_load(&client->client_id);
438
439 if (len == 0) {
440 // Empty reason - client disconnecting without explanation
441 log_info("Client %u sent leave notification (no reason)", client_id);
442 } else if (len <= 256) {
443 // Reason provided - extract and log it
444 if (!data) {
445 SET_ERRNO(ERROR_INVALID_STATE, "Client %u sent leave notification with non-zero length but NULL data", client_id);
446 return;
447 }
448
449 char reason[257] = {0};
450 memcpy(reason, data, len);
451 reason[len] = '\0';
452
453 // Validate reason is printable (handle potential non-UTF8 gracefully)
454 bool all_printable = true;
455 for (size_t i = 0; i < len; i++) {
456 uint8_t c = (uint8_t)reason[i];
457 if (c < 32 && c != '\t' && c != '\n') {
458 all_printable = false;
459 break;
460 }
461 }
462
463 if (all_printable) {
464 log_info("Client %u sent leave notification: %s", client_id, reason);
465 } else {
466 log_info("Client %u sent leave notification (reason contains non-printable characters)", client_id);
467 }
468 } else {
469 // Oversized reason - shouldn't happen with validation.h checks
470 log_warn("Client %u sent oversized leave reason (%zu bytes, max 256)", client_id, len);
471 }
472
473 // Deactivate client to stop processing packets
474 // Sets client->active = false immediately - triggers client cleanup procedures
475 log_debug("Setting active=false in handle_client_leave_packet (client_id=%u)", client_id);
476 atomic_store(&client->active, false);
477
478 // Note: We don't disconnect the client here - that happens when socket closes
479 // This is just a clean notification before disconnect
480}

◆ handle_image_frame_packet()

void handle_image_frame_packet ( client_info_t *  client,
void *  data,
size_t  len 
)

Process IMAGE_FRAME packet - store client's video data for rendering.

This is the most performance-critical packet handler, processing real-time video data from clients. It validates, stores, and tracks video frames for subsequent ASCII conversion and grid layout.

PACKET STRUCTURE EXPECTED:

  • uint32_t width (network byte order)
  • uint32_t height (network byte order)
  • rgb_pixel_t pixels[width * height] (RGB888 format)

PERFORMANCE CHARACTERISTICS:

  • Called at 30fps per active client
  • Uses zero-copy storage when possible
  • Validates packet size before processing
  • Implements frame counting for debug logging

STATE CHANGES PERFORMED:

  • Auto-enables client->is_sending_video if not already set
  • Increments client->frames_received counter
  • Updates client dimensions if changed

BUFFER MANAGEMENT:

  • Stores entire packet (including dimensions) in client->incoming_video_buffer
  • Uses multi-frame ringbuffer for burst handling
  • Buffer overflow drops oldest frames (maintains real-time performance)
  • render.c threads consume frames for ASCII conversion

VALIDATION PERFORMED:

  • Packet size matches width * height * 3 + 8 bytes
  • Width and height are reasonable (prevents memory exhaustion)
  • Buffer pointers are valid before access

ERROR HANDLING:

  • Invalid packets are logged and dropped
  • Buffer overflow is handled gracefully
  • Shutdown conditions don't generate error spam

PERFORMANCE OPTIMIZATIONS:

  • Debug logging is throttled (every 25000 frames)
  • Fast path for common case (valid packet with buffer space)
  • Minimal CPU work in receive thread (storage only)
Parameters
clientSource client providing video data
dataPacket payload containing image dimensions and RGB data
lenTotal size of packet payload in bytes
Note
This function is called by client receive threads at high frequency
Actual ASCII conversion happens in render threads (render.c)
Frame timestamps are added for synchronization purposes
See also
framebuffer_write_multi_frame() For buffer storage implementation
create_mixed_ascii_frame_for_client() For frame consumption

Definition at line 732 of file server/protocol.c.

732 {
733 // Handle incoming image data from client
734 // New format: [width:4][height:4][compressed_flag:4][data_size:4][rgb_data:data_size]
735 // Old format: [width:4][height:4][rgb_data:w*h*3] (for backward compatibility)
736 // Use atomic compare-and-swap to avoid race condition - ensures thread-safe auto-enabling of video stream
737
738 log_info("RECV_IMAGE_FRAME: client_id=%u, len=%zu", atomic_load(&client->client_id), len);
739
740 if (!data || len < sizeof(uint32_t) * 2) {
741 disconnect_client_for_bad_data(client, "IMAGE_FRAME payload too small: %zu bytes", len);
742 return;
743 }
744 bool was_sending_video = atomic_load(&client->is_sending_video);
745 if (!was_sending_video) {
746 // Try to atomically enable video sending
747 // Use atomic_compare_exchange_strong to avoid spurious failures
748 if (atomic_compare_exchange_strong(&client->is_sending_video, &was_sending_video, true)) {
749 log_info("Client %u auto-enabled video stream (received IMAGE_FRAME)", atomic_load(&client->client_id));
750 // Notify client that their first video frame was received
751 if (client->socket != INVALID_SOCKET_VALUE) {
752 log_info_client(client, "First video frame received - streaming active");
753 }
754 }
755 } else {
756 // Log periodically to confirm we're receiving frames
757 // Use per-client counter protected by client_state_mutex to avoid race conditions
758 mutex_lock(&client->client_state_mutex);
759 client->frames_received_logged++;
760 if (client->frames_received_logged % 25000 == 0) {
761 char pretty[64];
762 format_bytes_pretty(len, pretty, sizeof(pretty));
763 log_debug("Client %u has sent %u IMAGE_FRAME packets (%s)", atomic_load(&client->client_id),
764 client->frames_received_logged, pretty);
765 }
766 mutex_unlock(&client->client_state_mutex);
767 }
768
769 // Parse image dimensions (use memcpy to avoid unaligned access)
770 uint32_t img_width_net, img_height_net;
771 memcpy(&img_width_net, data, sizeof(uint32_t));
772 memcpy(&img_height_net, (char *)data + sizeof(uint32_t), sizeof(uint32_t));
773 uint32_t img_width = NET_TO_HOST_U32(img_width_net);
774 uint32_t img_height = NET_TO_HOST_U32(img_height_net);
775
776 log_debug("IMAGE_FRAME packet: width=%u, height=%u, payload_len=%zu", img_width, img_height, len);
777
778 // Validate dimensions using image utility functions
779 if (image_validate_dimensions((size_t)img_width, (size_t)img_height) != ASCIICHAT_OK) {
780 log_error("IMAGE_FRAME validation failed for dimensions: %u x %u", img_width, img_height);
781 disconnect_client_for_bad_data(client, "IMAGE_FRAME invalid dimensions");
782 return;
783 }
784
785 // Calculate RGB buffer size with overflow checking
786 size_t rgb_size = 0;
787 if (image_calc_rgb_size((size_t)img_width, (size_t)img_height, &rgb_size) != ASCIICHAT_OK) {
788 disconnect_client_for_bad_data(client, "IMAGE_FRAME buffer size calculation failed");
789 return;
790 }
791
792 // Validate final buffer size against maximum
793 if (image_validate_buffer_size(rgb_size) != ASCIICHAT_OK) {
794 disconnect_client_for_bad_data(client, "IMAGE_FRAME buffer size exceeds maximum");
795 return;
796 }
797
798 // Only support legacy format: [width:4][height:4][rgb_data:w*h*3]
799 if (rgb_size > SIZE_MAX - FRAME_HEADER_SIZE_LEGACY) {
800 char size_str[32];
801 format_bytes_pretty(rgb_size, size_str, sizeof(size_str));
802 disconnect_client_for_bad_data(client, "IMAGE_FRAME legacy packet size overflow: %s", size_str);
803 return;
804 }
805 size_t expected_size = FRAME_HEADER_SIZE_LEGACY + rgb_size;
806
807 if (len != expected_size) {
808 disconnect_client_for_bad_data(client, "IMAGE_FRAME size mismatch: expected %zu bytes got %zu", expected_size, len);
809 return;
810 }
811
812 // Validate legacy format
813 asciichat_error_t validate_result = frame_validate_legacy(len, rgb_size);
814 if (validate_result != ASCIICHAT_OK) {
815 disconnect_client_for_bad_data(client, "IMAGE_FRAME legacy validation failed");
816 return;
817 }
818
819 void *rgb_data = (char *)data + FRAME_HEADER_SIZE_LEGACY;
820 size_t rgb_data_size = rgb_size;
821 bool needs_free = false;
822
823 if (client->incoming_video_buffer) {
824 // Get the write buffer
825 video_frame_t *frame = video_frame_begin_write(client->incoming_video_buffer);
826
827 if (frame && frame->data) {
828 // Build the packet in the old format for internal storage: [width:4][height:4][rgb_data:w*h*3]
829 // Use frame_check_size_overflow to validate overflow before repacking
830 asciichat_error_t overflow_check = frame_check_size_overflow(FRAME_HEADER_SIZE_LEGACY, rgb_data_size);
831 if (overflow_check != ASCIICHAT_OK) {
832 if (needs_free && rgb_data) {
833 SAFE_FREE(rgb_data);
834 }
835 disconnect_client_for_bad_data(client, "IMAGE_FRAME size overflow while repacking");
836 return;
837 }
838 size_t old_packet_size = FRAME_HEADER_SIZE_LEGACY + rgb_data_size;
839
840 if (old_packet_size <= MAX_FRAME_BUFFER_SIZE) { // Max frame buffer size
841 uint32_t width_net = HOST_TO_NET_U32(img_width);
842 uint32_t height_net = HOST_TO_NET_U32(img_height);
843
844 // Pack in old format for internal consistency
845 memcpy(frame->data, &width_net, sizeof(uint32_t));
846 memcpy((char *)frame->data + sizeof(uint32_t), &height_net, sizeof(uint32_t));
847 memcpy((char *)frame->data + sizeof(uint32_t) * 2, rgb_data, rgb_data_size);
848
849 frame->size = old_packet_size;
850 frame->width = img_width;
851 frame->height = img_height;
852 frame->capture_timestamp_ns = (uint64_t)time(NULL) * NS_PER_SEC_INT;
853 frame->sequence_number = ++client->frames_received;
854
855 // DEBUG: Compute hash of incoming RGB data to detect duplicates
856 uint32_t incoming_rgb_hash = 0;
857 for (size_t i = 0; i < rgb_data_size && i < 1000; i++) {
858 incoming_rgb_hash = (uint32_t)((uint64_t)incoming_rgb_hash * 31 + ((unsigned char *)rgb_data)[i]);
859 }
860
861 // Per-client hash tracking (not static!) to avoid cross-client interference
862 uint32_t client_id = atomic_load(&client->client_id);
863 bool is_new_frame = (incoming_rgb_hash != client->last_received_frame_hash);
864
865 if (is_new_frame) {
866 log_info("RECV_FRAME #%u NEW: Client %u size=%zu dims=%ux%u hash=0x%08x (prev=0x%08x)",
867 client->frames_received, client_id, rgb_data_size, img_width, img_height, incoming_rgb_hash,
868 client->last_received_frame_hash);
869 client->last_received_frame_hash = incoming_rgb_hash;
870 } else {
871 log_info("RECV_FRAME #%u DUP: Client %u size=%zu dims=%ux%u hash=0x%08x", client->frames_received, client_id,
872 rgb_data_size, img_width, img_height, incoming_rgb_hash);
873 }
874
875 video_frame_commit(client->incoming_video_buffer);
876 } else {
877 if (needs_free && rgb_data) {
878 SAFE_FREE(rgb_data);
879 }
880 disconnect_client_for_bad_data(client, "IMAGE_FRAME repacked frame too large (%zu bytes)", old_packet_size);
881 return;
882 }
883 } else {
884 log_warn("Failed to get write buffer for client %u (frame=%p, frame->data=%p)", atomic_load(&client->client_id),
885 (void *)frame, frame ? frame->data : NULL);
886 }
887 } else {
888 // During shutdown, this is expected - don't spam error logs
889 if (!atomic_load(&g_server_should_exit)) {
890 SET_ERRNO(ERROR_INVALID_STATE, "Client %u has no incoming video buffer!", atomic_load(&client->client_id));
891 } else {
892 log_debug("Client %u: ignoring video packet during shutdown", atomic_load(&client->client_id));
893 }
894 }
895
896 // Clean up decompressed data if allocated
897 if (needs_free && rgb_data) {
898 SAFE_FREE(rgb_data);
899 }
900}
asciichat_error_t frame_validate_legacy(size_t len, size_t expected_rgb_size)
asciichat_error_t frame_check_size_overflow(size_t header_size, size_t data_size)
atomic_bool g_server_should_exit
Global atomic shutdown flag shared across all threads.
void format_bytes_pretty(size_t bytes, char *out, size_t out_capacity)
Definition util/format.c:10
asciichat_error_t image_validate_buffer_size(size_t requested_size)
Definition util/image.c:115
asciichat_error_t image_calc_rgb_size(size_t width, size_t height, size_t *out_size)
Definition util/image.c:54
asciichat_error_t image_validate_dimensions(size_t width, size_t height)
Definition video.c:12
video_frame_t * video_frame_begin_write(video_frame_buffer_t *vfb)
void video_frame_commit(video_frame_buffer_t *vfb)

References disconnect_client_for_bad_data(), format_bytes_pretty(), frame_check_size_overflow(), frame_validate_legacy(), g_server_should_exit, image_calc_rgb_size(), image_validate_buffer_size(), image_validate_dimensions(), video_frame_begin_write(), and video_frame_commit().

◆ handle_ping_packet()

void handle_ping_packet ( client_info_t *  client,
const void *  data,
size_t  len 
)

Handle PING packet - respond with PONG.

Definition at line 642 of file server/protocol.c.

642 {
643 (void)data;
644 (void)len;
645
646 // Get transport reference briefly to avoid deadlock on TCP buffer full
647 mutex_lock(&client->send_mutex);
648 if (atomic_load(&client->shutting_down) || !client->transport) {
649 mutex_unlock(&client->send_mutex);
650 return;
651 }
652 acip_transport_t *pong_transport = client->transport;
653 mutex_unlock(&client->send_mutex);
654
655 // Network I/O happens OUTSIDE the mutex
656 asciichat_error_t pong_result = acip_send_pong(pong_transport);
657 if (pong_result != ASCIICHAT_OK) {
658 SET_ERRNO(ERROR_NETWORK, "Failed to send PONG response to client %u: %s", atomic_load(&client->client_id),
659 asciichat_error_string(pong_result));
660 }
661}
asciichat_error_t acip_send_pong(acip_transport_t *transport)
Definition send.c:212

References acip_send_pong().

◆ handle_pong_packet()

void handle_pong_packet ( client_info_t *  client,
const void *  data,
size_t  len 
)

Handle PONG packet - client acknowledged our PING.

Definition at line 666 of file server/protocol.c.

666 {
667 (void)client;
668 (void)data;
669 (void)len;
670 // No action needed - client acknowledged our PING
671}

◆ handle_protocol_version_packet()

void handle_protocol_version_packet ( client_info_t *  client,
const void *  data,
size_t  len 
)

Process PROTOCOL_VERSION packet - validate protocol compatibility.

Clients send this packet to announce their protocol version and capabilities. The server validates that the major version matches and logs any version mismatches for debugging purposes.

PACKET STRUCTURE EXPECTED:

  • protocol_version_packet_t containing:
    • protocol_version: Major version number (must match PROTOCOL_VERSION_MAJOR)
    • protocol_revision: Minor version number
    • supports_encryption: Encryption capability flag
    • compression_algorithms: Supported compression bitmask
    • feature_flags: Optional feature flags

VALIDATION PERFORMED:

  • Packet size matches sizeof(protocol_version_packet_t)
  • Major protocol version matches (PROTOCOL_VERSION_MAJOR)
  • Reserved bytes are zero (future-proofing)

ERROR HANDLING:

  • Version mismatch is logged but NOT fatal (backward compatibility)
  • Invalid packet size triggers disconnect
Parameters
clientClient that sent the packet
dataPacket payload (protocol_version_packet_t)
lenSize of packet payload in bytes
Note
This is typically the first packet in the handshake
Protocol version validation ensures compatibility

Definition at line 355 of file server/protocol.c.

355 {
356 if (!data) {
357 disconnect_client_for_bad_data(client, "PROTOCOL_VERSION payload missing");
358 return;
359 }
360
361 if (len != sizeof(protocol_version_packet_t)) {
362 disconnect_client_for_bad_data(client, "PROTOCOL_VERSION invalid size: %zu (expected %zu)", len,
363 sizeof(protocol_version_packet_t));
364 return;
365 }
366
367 const protocol_version_packet_t *version = (const protocol_version_packet_t *)data;
368 uint16_t client_major = NET_TO_HOST_U16(version->protocol_version);
369 uint16_t client_minor = NET_TO_HOST_U16(version->protocol_revision);
370
371 // Validate major version match (minor version can differ for backward compat)
372 if (client_major != PROTOCOL_VERSION_MAJOR) {
373 log_warn("Client %u protocol version mismatch: client=%u.%u, server=%u.%u", atomic_load(&client->client_id),
374 client_major, client_minor, PROTOCOL_VERSION_MAJOR, PROTOCOL_VERSION_MINOR);
375 // Note: We don't disconnect on version mismatch for backward compatibility
376 // Clients may be older or newer than server
377 } else if (client_minor != PROTOCOL_VERSION_MINOR) {
378 log_info("Client %u has different protocol revision: client=%u.%u, server=%u.%u", atomic_load(&client->client_id),
379 client_major, client_minor, PROTOCOL_VERSION_MAJOR, PROTOCOL_VERSION_MINOR);
380 }
381
382 // Validate reserved bytes are zero
383 for (size_t i = 0; i < sizeof(version->reserved); i++) {
384 if (version->reserved[i] != 0) {
385 log_warn("Client %u sent non-zero reserved bytes in PROTOCOL_VERSION packet", atomic_load(&client->client_id));
386 // Don't disconnect - reserved bytes may be used in future versions
387 break;
388 }
389 }
390
391 // Log supported features
392 if (ACIP_CRYPTO_HAS_ENCRYPT(version->supports_encryption)) {
393 log_debug("Client %u supports encryption", atomic_load(&client->client_id));
394 }
395 if (version->compression_algorithms != 0) {
396 log_debug("Client %u supports compression: 0x%02x", atomic_load(&client->client_id),
397 version->compression_algorithms);
398 }
399 if (version->feature_flags != 0) {
400 uint16_t feature_flags = NET_TO_HOST_U16(version->feature_flags);
401 log_debug("Client %u supports features: 0x%04x", atomic_load(&client->client_id), feature_flags);
402 }
403}

References disconnect_client_for_bad_data().

◆ handle_remote_log_packet_from_client()

void handle_remote_log_packet_from_client ( client_info_t *  client,
const void *  data,
size_t  len 
)

Definition at line 959 of file server/protocol.c.

959 {
960 if (!client) {
961 return;
962 }
963
964 log_level_t remote_level = LOG_INFO;
965 remote_log_direction_t direction = REMOTE_LOG_DIRECTION_UNKNOWN;
966 uint16_t flags = 0;
967 char message[MAX_REMOTE_LOG_MESSAGE_LENGTH + 1] = {0};
968
969 asciichat_error_t parse_result =
970 packet_parse_remote_log(data, len, &remote_level, &direction, &flags, message, sizeof(message), NULL);
971 if (parse_result != ASCIICHAT_OK) {
972 disconnect_client_for_bad_data(client, "Invalid REMOTE_LOG packet: %s", asciichat_error_string(parse_result));
973 return;
974 }
975
976 if (direction != REMOTE_LOG_DIRECTION_CLIENT_TO_SERVER) {
977 disconnect_client_for_bad_data(client, "REMOTE_LOG direction mismatch: %u", direction);
978 return;
979 }
980
981 const bool truncated = (flags & REMOTE_LOG_FLAG_TRUNCATED) != 0;
982 const char *display_name = client->display_name[0] ? client->display_name : "(unnamed)";
983 uint32_t client_id = atomic_load(&client->client_id);
984
985 if (truncated) {
986 log_msg(remote_level, __FILE__, __LINE__, __func__, "[REMOTE CLIENT %u \"%s\"] %s [message truncated]", client_id,
987 display_name, message);
988 } else {
989 log_msg(remote_level, __FILE__, __LINE__, __func__, "[REMOTE CLIENT %u \"%s\"] %s", client_id, display_name,
990 message);
991 }
992}
void log_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt,...)
asciichat_error_t packet_parse_remote_log(const void *data, size_t len, log_level_t *out_level, remote_log_direction_t *out_direction, uint16_t *out_flags, char *message_buffer, size_t message_buffer_size, size_t *out_message_length)
Definition packet.c:950

References disconnect_client_for_bad_data(), log_msg(), and packet_parse_remote_log().

◆ handle_size_packet()

void handle_size_packet ( client_info_t *  client,
const void *  data,
size_t  len 
)

Process terminal size update packet - handle client window resize.

Clients send this packet when their terminal window is resized, allowing the server to adjust ASCII frame dimensions accordingly. This ensures optimal use of the client's display area.

PACKET STRUCTURE EXPECTED:

  • size_packet_t containing:
    • uint16_t width: New terminal width in characters
    • uint16_t height: New terminal height in characters

STATE CHANGES PERFORMED:

  • Updates client->width with new dimensions
  • Updates client->height with new dimensions
  • Thread-safe update using client state mutex

RENDERING IMPACT:

  • Subsequent ASCII frames will use new dimensions
  • Grid layout calculations will incorporate new size
  • No immediate frame regeneration (happens on next cycle)

ERROR HANDLING:

  • Invalid packet sizes are ignored silently
  • Extreme dimensions are accepted (client responsibility)
  • Concurrent updates are handled safely
Parameters
clientTarget client whose terminal was resized
dataPacket payload containing new dimensions
lenSize of packet payload (should be sizeof(size_packet_t))
Note
Changes take effect on the next rendering cycle
No validation of reasonable dimension ranges

Definition at line 1612 of file server/protocol.c.

1612 {
1613 VALIDATE_PACKET_SIZE(client, data, len, sizeof(size_packet_t), "SIZE");
1614
1615 const size_packet_t *size_pkt = (const size_packet_t *)data;
1616
1617 // Extract and validate new dimensions
1618 uint16_t width = NET_TO_HOST_U16(size_pkt->width);
1619 uint16_t height = NET_TO_HOST_U16(size_pkt->height);
1620
1621 VALIDATE_NONZERO(client, width, "width", "SIZE");
1622 VALIDATE_NONZERO(client, height, "height", "SIZE");
1623 VALIDATE_RANGE(client, width, 1, 4096, "width", "SIZE");
1624 VALIDATE_RANGE(client, height, 1, 4096, "height", "SIZE");
1625
1626 mutex_lock(&client->client_state_mutex);
1627 client->width = width;
1628 client->height = height;
1629 mutex_unlock(&client->client_state_mutex);
1630
1631 log_info("Client %u updated terminal size: %ux%u", atomic_load(&client->client_id), width, height);
1632}

◆ handle_stream_start_packet()

void handle_stream_start_packet ( client_info_t *  client,
const void *  data,
size_t  len 
)

Process STREAM_START packet - client requests to begin media transmission.

Clients send this packet to indicate they're ready to start sending video and/or audio data. The server updates its internal state to expect and process media packets from this client.

PACKET STRUCTURE EXPECTED:

  • uint32_t stream_type (network byte order)
  • Bitmask containing STREAM_TYPE_VIDEO and/or STREAM_TYPE_AUDIO

STATE CHANGES PERFORMED:

  • VIDEO: Records intention to send video (is_sending_video set by first IMAGE_FRAME)
  • AUDIO: Sets client->is_sending_audio = true if STREAM_TYPE_AUDIO present
  • Enables render threads to include this client in output generation

PROTOCOL BEHAVIOR:

  • Client must have announced capabilities via CLIENT_JOIN first
  • Server will start processing IMAGE_FRAME and AUDIO packets
  • Render threads will begin generating output for this client
  • Grid layout will be recalculated to include this client

ERROR HANDLING:

  • Ignores packets with incorrect size
  • Invalid stream types are silently ignored
  • Graceful handling if client lacks necessary capabilities
Parameters
clientTarget client starting media transmission
dataPacket payload containing stream type flags
lenSize of packet payload (should be sizeof(uint32_t))
Note
Changes take effect immediately for subsequent media packets
Render threads will detect the state change on their next cycle
See also
handle_stream_stop_packet() For stopping media transmission
handle_image_frame_packet() For Video to ASCII Conversion data processing

Definition at line 518 of file server/protocol.c.

518 {
519 VALIDATE_PACKET_SIZE(client, data, len, sizeof(uint32_t), "STREAM_START");
520
521 uint32_t stream_type_net;
522 memcpy(&stream_type_net, data, sizeof(uint32_t));
523 uint32_t stream_type = NET_TO_HOST_U32(stream_type_net);
524
525 // Validate at least one stream type flag is set
526 const uint32_t VALID_STREAM_MASK = STREAM_TYPE_VIDEO | STREAM_TYPE_AUDIO;
527 VALIDATE_CAPABILITY_FLAGS(client, stream_type, VALID_STREAM_MASK, "STREAM_START");
528
529 // Validate no unknown stream type bits are set
530 VALIDATE_FLAGS_MASK(client, stream_type, VALID_STREAM_MASK, "STREAM_START");
531
532 if (stream_type & STREAM_TYPE_VIDEO) {
533 atomic_store(&client->is_sending_video, true);
534 }
535 if (stream_type & STREAM_TYPE_AUDIO) {
536 atomic_store(&client->is_sending_audio, true);
537
538 // Create Opus decoder for this client if not already created
539 if (!client->opus_decoder) {
540 client->opus_decoder = opus_codec_create_decoder(48000);
541 if (client->opus_decoder) {
542 log_info("Client %u: Opus decoder created (48kHz)", atomic_load(&client->client_id));
543 } else {
544 log_error("Client %u: Failed to create Opus decoder", atomic_load(&client->client_id));
545 }
546 }
547 }
548
549 if (stream_type & STREAM_TYPE_VIDEO) {
550 log_info("Client %u announced video stream (waiting for first frame)", atomic_load(&client->client_id));
551 }
552 if (stream_type & STREAM_TYPE_AUDIO) {
553 log_info("Client %u started audio stream", atomic_load(&client->client_id));
554 }
555
556 // Notify client of stream start acknowledgment
557 const char *streams = (stream_type & STREAM_TYPE_VIDEO) && (stream_type & STREAM_TYPE_AUDIO)
558 ? "video+audio"
559 : ((stream_type & STREAM_TYPE_VIDEO) ? "video" : "audio");
560 // Only send remote log to TCP clients (WebSocket clients have invalid socket)
561 if (client->socket != INVALID_SOCKET_VALUE) {
562 log_info_client(client, "Stream started: %s", streams);
563 }
564}
opus_codec_t * opus_codec_create_decoder(int sample_rate)
Definition opus_codec.c:62

References opus_codec_create_decoder().

◆ handle_stream_stop_packet()

void handle_stream_stop_packet ( client_info_t *  client,
const void *  data,
size_t  len 
)

Process STREAM_STOP packet - client requests to halt media transmission.

Clients send this packet to gracefully stop sending video and/or audio data. The server updates its state to exclude this client from active media processing and grid layout calculations.

PACKET STRUCTURE EXPECTED:

  • uint32_t stream_type (network byte order)
  • Bitmask containing STREAM_TYPE_VIDEO and/or STREAM_TYPE_AUDIO

STATE CHANGES PERFORMED:

  • Sets client->is_sending_video = false if STREAM_TYPE_VIDEO present
  • Sets client->is_sending_audio = false if STREAM_TYPE_AUDIO present
  • Render threads will stop including this client in output

PROTOCOL BEHAVIOR:

  • Client remains connected but won't appear in video grid
  • Existing buffered media from this client will still be processed
  • Grid layout recalculates to exclude this client
  • Client can restart streaming with STREAM_START packet

ERROR HANDLING:

  • Ignores packets with incorrect size
  • Invalid stream types are silently ignored
  • Safe to call multiple times or when not streaming
Parameters
clientTarget client stopping media transmission
dataPacket payload containing stream type flags
lenSize of packet payload (should be sizeof(uint32_t))
Note
Changes take effect immediately
Render threads will detect the state change on their next cycle
See also
handle_stream_start_packet() For starting media transmission

Definition at line 601 of file server/protocol.c.

601 {
602 VALIDATE_PACKET_SIZE(client, data, len, sizeof(uint32_t), "STREAM_STOP");
603
604 uint32_t stream_type_net;
605 memcpy(&stream_type_net, data, sizeof(uint32_t));
606 uint32_t stream_type = NET_TO_HOST_U32(stream_type_net);
607
608 // Validate at least one stream type flag is set
609 const uint32_t VALID_STREAM_MASK = STREAM_TYPE_VIDEO | STREAM_TYPE_AUDIO;
610 VALIDATE_CAPABILITY_FLAGS(client, stream_type, VALID_STREAM_MASK, "STREAM_STOP");
611
612 // Validate no unknown stream type bits are set
613 VALIDATE_FLAGS_MASK(client, stream_type, VALID_STREAM_MASK, "STREAM_STOP");
614
615 if (stream_type & STREAM_TYPE_VIDEO) {
616 atomic_store(&client->is_sending_video, false);
617 }
618 if (stream_type & STREAM_TYPE_AUDIO) {
619 atomic_store(&client->is_sending_audio, false);
620 }
621
622 if (stream_type & STREAM_TYPE_VIDEO) {
623 log_info("Client %u stopped video stream", atomic_load(&client->client_id));
624 }
625 if (stream_type & STREAM_TYPE_AUDIO) {
626 log_info("Client %u stopped audio stream", atomic_load(&client->client_id));
627 }
628
629 // Notify client of stream stop acknowledgment
630 const char *streams = (stream_type & STREAM_TYPE_VIDEO) && (stream_type & STREAM_TYPE_AUDIO)
631 ? "video+audio"
632 : ((stream_type & STREAM_TYPE_VIDEO) ? "video" : "audio");
633 // Only send remote log to TCP clients (WebSocket clients have invalid socket)
634 if (client->socket != INVALID_SOCKET_VALUE) {
635 log_info_client(client, "Stream stopped: %s", streams);
636 }
637}

◆ send_server_state_to_client()

int send_server_state_to_client ( client_info_t *  client)

Send current server state to a specific client.

Generates and queues a SERVER_STATE packet containing information about the current number of connected and active clients. This helps clients understand the multi-user environment and adjust their behavior accordingly.

PACKET CONTENT GENERATED:

  • server_state_packet_t containing:
    • connected_client_count: Total clients connected to server
    • active_client_count: Clients actively sending video/audio
    • reserved: Padding for future extensions

USAGE SCENARIOS:

  • Initial state after client joins server
  • Periodic updates when client count changes
  • Response to client requests for server information

IMPLEMENTATION DETAILS:

  • Counts active clients by scanning global client manager
  • Converts data to network byte order before queuing
  • Uses client's video queue for delivery
  • Non-blocking operation (queues for later delivery)

THREAD SAFETY:

  • Uses reader lock on global client manager
  • Safe to call from any thread
  • Atomic snapshot of client counts

ERROR HANDLING:

  • Returns -1 if client or queue is invalid
  • Queue overflow handled by packet_queue_enqueue()
  • No side effects on failure
Parameters
clientTarget client to receive server state information
Returns
0 on successful queuing, -1 on error
Note
Packet delivery happens asynchronously via send thread
Client count may change between queuing and delivery
See also
broadcast_server_state_to_all_clients() For multi-client updates

Definition at line 1686 of file server/protocol.c.

1686 {
1687 if (!client) {
1688 return -1;
1689 }
1690
1691 // Count active clients - LOCK OPTIMIZATION: Use atomic reads, no rwlock needed
1692 int active_count = 0;
1693 for (int i = 0; i < MAX_CLIENTS; i++) {
1694 if (atomic_load(&g_client_manager.clients[i].active)) {
1695 active_count++;
1696 }
1697 }
1698
1699 // Prepare server state packet
1700 server_state_packet_t state;
1701 state.connected_client_count = active_count;
1702 state.active_client_count = active_count; // For now, all connected are active
1703 memset(state.reserved, 0, sizeof(state.reserved));
1704
1705 // Convert to network byte order
1706 server_state_packet_t net_state;
1707 net_state.connected_client_count = HOST_TO_NET_U32(state.connected_client_count);
1708 net_state.active_client_count = HOST_TO_NET_U32(state.active_client_count);
1709 memset(net_state.reserved, 0, sizeof(net_state.reserved));
1710
1711 // Send server state via ACIP transport
1712 // Protect socket writes with send_mutex to prevent race with send_thread.
1713 mutex_lock(&client->send_mutex);
1714 asciichat_error_t result = acip_send_server_state(client->transport, &net_state);
1715 mutex_unlock(&client->send_mutex);
1716
1717 if (result != ASCIICHAT_OK) {
1718 SET_ERRNO(ERROR_NETWORK, "Failed to send server state to client %u: %s", client->client_id,
1719 asciichat_error_string(result));
1720 return -1;
1721 }
1722
1723 log_debug("Sent server state to client %u: %u connected, %u active", client->client_id, state.connected_client_count,
1724 state.active_client_count);
1725 return 0;
1726}
asciichat_error_t acip_send_server_state(acip_transport_t *transport, const server_state_packet_t *state)
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 acip_send_server_state(), client_manager_t::clients, and g_client_manager.

Referenced by add_webrtc_client().