131#include <stdatomic.h>
141#include <ascii-chat/common.h>
142#include <ascii-chat/util/endian.h>
143#include <ascii-chat/buffer_pool.h>
144#include <ascii-chat/network/packet_queue.h>
145#include <ascii-chat/ringbuffer.h>
146#include <ascii-chat/video/video_frame.h>
147#include <ascii-chat/video/image.h>
148#include <ascii-chat/video/ascii.h>
149#include <ascii-chat/video/color_filter.h>
150#include <ascii-chat/util/aspect_ratio.h>
151#include <ascii-chat/util/endian.h>
152#include <ascii-chat/util/time.h>
153#include <ascii-chat/util/image.h>
164static atomic_int g_previous_active_video_count = 0;
220static int collect_video_sources(
image_source_t *sources,
int max_sources) {
221 int source_count = 0;
232 bool is_sending_video;
233 video_frame_buffer_t *video_buffer;
236 client_snapshot_t client_snapshots[MAX_CLIENTS];
237 int snapshot_count = 0;
240 for (
int i = 0; i < MAX_CLIENTS; i++) {
243 if (atomic_load(&client->client_id) == 0) {
248 client_snapshots[snapshot_count].client_id = atomic_load(&client->client_id);
249 client_snapshots[snapshot_count].is_active = atomic_load(&client->active);
250 client_snapshots[snapshot_count].is_sending_video = atomic_load(&client->is_sending_video);
251 client_snapshots[snapshot_count].video_buffer = client->incoming_video_buffer;
256 log_dev_every(5 * NS_PER_MS_INT,
"collect_video_sources: Processing %d snapshots", snapshot_count);
257 for (
int i = 0; i < snapshot_count && source_count < max_sources; i++) {
258 client_snapshot_t *snap = &client_snapshots[i];
260 if (!snap->is_active) {
261 log_dev_every(5 * NS_PER_MS_INT,
"collect_video_sources: Skipping inactive client %u", snap->client_id);
265 log_dev_every(5 * NS_PER_MS_INT,
"collect_video_sources: Client %u: is_sending_video=%d", snap->client_id,
266 snap->is_sending_video);
268 sources[source_count].
client_id = snap->client_id;
269 sources[source_count].
image = NULL;
273 multi_source_frame_t current_frame = {0};
274 bool got_new_frame =
false;
278 if (snap->is_sending_video && snap->video_buffer) {
287 void *frame_data_ptr = frame->data;
289 size_t frame_size_val = frame->size;
292 uint32_t incoming_hash = 0;
293 if (frame_data_ptr && frame_size_val > 0) {
294 for (
size_t i = 0; i < frame_size_val && i < 1000; i++) {
295 uint8_t
byte = ((
unsigned char *)frame_data_ptr)[i];
296 incoming_hash = (uint32_t)((uint64_t)incoming_hash * 31 + byte);
301 static uint32_t last_buffer_hash = 0;
302 if (incoming_hash != last_buffer_hash) {
303 log_info(
"BUFFER_FRAME CHANGE: Client %u got NEW frame from buffer: hash=0x%08x (prev=0x%08x) size=%zu",
304 snap->client_id, incoming_hash, last_buffer_hash, frame_size_val);
305 last_buffer_hash = incoming_hash;
307 log_dev_every(25000,
"BUFFER_FRAME DUPLICATE: Client %u frame hash=0x%08x size=%zu (no change)",
308 snap->client_id, incoming_hash, frame_size_val);
312 if (frame_data_ptr && frame_size_val >= 8) {
313 uint32_t width_net, height_net;
314 memcpy(&width_net, frame_data_ptr,
sizeof(uint32_t));
315 memcpy(&height_net, (
char *)frame_data_ptr +
sizeof(uint32_t),
sizeof(uint32_t));
316 uint32_t width = NET_TO_HOST_U32(width_net);
317 uint32_t height = NET_TO_HOST_U32(height_net);
320 uint8_t *pixel_ptr = (uint8_t *)frame_data_ptr + 8;
321 uint32_t first_pixel_rgb = 0;
322 if (frame_size_val >= 11) {
323 first_pixel_rgb = ((uint32_t)pixel_ptr[0] << 16) | ((uint32_t)pixel_ptr[1] << 8) | (uint32_t)pixel_ptr[2];
326 log_info(
"BUFFER_INSPECT: Client %u dims=%ux%u pixel_data_size=%zu first_pixel_rgb=0x%06x data_hash=0x%08x",
327 snap->client_id, width, height, frame_size_val - 8, first_pixel_rgb, incoming_hash);
330 log_debug_every(5 * NS_PER_MS_INT,
"Video mixer: client %u incoming frame hash=0x%08x size=%zu", snap->client_id,
331 incoming_hash, frame_size_val);
333 if (frame_data_ptr && frame_size_val > 0 && frame_size_val >= (
sizeof(uint32_t) * 2 + 3)) {
336 uint32_t peek_width = NET_TO_HOST_U32(read_u32_unaligned(frame_data_ptr));
337 uint32_t peek_height = NET_TO_HOST_U32(read_u32_unaligned(frame_data_ptr +
sizeof(uint32_t)));
340 if (peek_width == 0 || peek_height == 0 || peek_width > 4096 || peek_height > 2160) {
341 log_debug(
"Per-client %u: rejected dimensions %ux%u as corrupted", snap->client_id, peek_width, peek_height);
351 size_t correct_frame_size =
sizeof(uint32_t) * 2;
354 if (
image_calc_rgb_size((
size_t)peek_width, (
size_t)peek_height, &rgb_size) != ASCIICHAT_OK) {
355 log_debug(
"Per-client: rgb_size calc failed for %ux%u", peek_width, peek_height);
358 correct_frame_size += rgb_size;
361 log_debug_every(NS_PER_MS_INT,
"Per-client: frame dimensions=%ux%u, frame_size=%zu, correct_size=%zu",
362 peek_width, peek_height, frame_size_val, correct_frame_size);
365 if (frame_size_val < correct_frame_size) {
366 log_debug(
"Per-client: frame too small: got %zu, need %zu", frame_size_val, correct_frame_size);
372 current_frame.data = SAFE_MALLOC(correct_frame_size,
void *);
374 if (current_frame.data) {
375 memcpy(current_frame.data, frame->data, correct_frame_size);
376 current_frame.size = correct_frame_size;
377 current_frame.source_client_id = snap->client_id;
378 current_frame.timestamp = (uint32_t)(frame->capture_timestamp_ns / NS_PER_SEC_INT);
379 got_new_frame =
true;
385 multi_source_frame_t *frame_to_use = got_new_frame ? ¤t_frame : NULL;
387 if (frame_to_use && frame_to_use->data && frame_to_use->size >
sizeof(uint32_t) * 2) {
391 uint32_t img_width = NET_TO_HOST_U32(read_u32_unaligned(frame_to_use->data));
392 uint32_t img_height = NET_TO_HOST_U32(read_u32_unaligned(frame_to_use->data +
sizeof(uint32_t)));
395 if (img_width == 0xBEBEBEBE || img_height == 0xBEBEBEBE) {
396 SET_ERRNO(ERROR_INVALID_STATE,
"UNINITIALIZED MEMORY DETECTED! First 16 bytes of frame data:");
397 uint8_t *bytes = (uint8_t *)frame_to_use->data;
398 SET_ERRNO(ERROR_INVALID_STATE,
399 " %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", bytes[0],
400 bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10],
401 bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]);
406 SET_ERRNO(ERROR_INVALID_STATE,
407 "Per-client: Invalid image dimensions from client %u: %ux%u (data may be corrupted)", snap->client_id,
408 img_width, img_height);
414 size_t expected_size =
sizeof(uint32_t) * 2;
417 if (
image_calc_rgb_size((
size_t)img_width, (
size_t)img_height, &rgb_size) != ASCIICHAT_OK) {
418 SET_ERRNO(ERROR_INVALID_STATE,
"Per-client: RGB size calculation failed for client %u: %ux%u",
419 snap->client_id, img_width, img_height);
423 expected_size += rgb_size;
425 if (frame_to_use->size != expected_size) {
426 SET_ERRNO(ERROR_INVALID_STATE,
427 "Per-client: Frame size mismatch from client %u: got %zu, expected %zu for %ux%u image",
428 snap->client_id, frame_to_use->size, expected_size, img_width, img_height);
434 rgb_pixel_t *pixels = (rgb_pixel_t *)(frame_to_use->data + (
sizeof(uint32_t) * 2));
439 log_error(
"Per-client: image_new_from_pool failed for %ux%u", img_width, img_height);
442 memcpy(img->pixels, pixels, (
size_t)img_width * (
size_t)img_height *
sizeof(rgb_pixel_t));
443 sources[source_count].
image = img;
448 SAFE_FREE(current_frame.data);
452 if (got_new_frame && current_frame.data) {
453 SAFE_FREE(current_frame.data);
474static image_t *create_single_source_composite(
image_source_t *sources,
int source_count,
479 image_t *single_source = NULL;
480 for (
int i = 0; i < source_count; i++) {
481 if (sources[i].has_video && sources[i].image) {
482 single_source = sources[i].
image;
487 if (!single_source) {
488 SET_ERRNO(ERROR_INVALID_STATE,
"Logic error: sources_with_video=1 but no source found");
497 return single_source;
521static void calculate_optimal_grid_layout(
image_source_t *sources,
int source_count,
int sources_with_video,
522 int terminal_width,
int terminal_height,
int *out_cols,
int *out_rows) {
524 if (sources_with_video == 0) {
530 if (sources_with_video == 1) {
541 float avg_aspect = 0.0f;
542 int aspect_count = 0;
543 for (
int i = 0; i < source_count; i++) {
544 if (sources[i].has_video && sources[i].image) {
545 float aspect = (float)sources[i].image->w / (
float)sources[i].
image->h;
546 avg_aspect += aspect;
550 if (aspect_count > 0) {
551 avg_aspect /= aspect_count;
558 int best_rows = sources_with_video;
559 float best_utilization = 0.0f;
562 for (
int cols = 1; cols <= sources_with_video; cols++) {
563 int rows = (sources_with_video + cols - 1) / cols;
566 int total_cells = cols * rows;
567 int empty_cells = total_cells - sources_with_video;
568 if (empty_cells > cols) {
573 int cell_width = terminal_width / cols;
574 int cell_height = terminal_height / rows;
577 if (cell_width < 20 || cell_height < 10) {
584 float total_area_used = 0.0f;
585 int cell_area = cell_width * cell_height;
587 for (
int i = 0; i < sources_with_video; i++) {
589 float video_aspect = avg_aspect;
594 float cell_visual_aspect = (float)cell_width / ((
float)cell_height *
CHAR_ASPECT);
597 int fitted_width, fitted_height;
599 if (video_aspect > cell_visual_aspect) {
601 fitted_width = cell_width;
604 fitted_height = (int)((cell_width / video_aspect) /
CHAR_ASPECT);
607 fitted_height = cell_height;
609 fitted_width = (int)(cell_height *
CHAR_ASPECT * video_aspect);
613 if (fitted_width > cell_width) {
614 fitted_width = cell_width;
616 if (fitted_height > cell_height) {
617 fitted_height = cell_height;
621 total_area_used += fitted_width * fitted_height;
625 float total_available_area = (float)(cell_area * sources_with_video);
626 float utilization = total_area_used / total_available_area;
628 float test_cell_visual_aspect = (float)cell_width / ((
float)cell_height *
CHAR_ASPECT);
629 log_dev_every(LOG_RATE_NORMAL,
" Testing %dx%d: cell=%dx%d (visual aspect %.2f), utilization=%.1f%%", cols, rows,
630 cell_width, cell_height, test_cell_visual_aspect, utilization * 100.0f);
633 if (utilization > best_utilization) {
634 best_utilization = utilization;
640 *out_cols = best_cols;
641 *out_rows = best_rows;
643 float terminal_visual_aspect = (float)terminal_width / ((
float)terminal_height *
CHAR_ASPECT);
644 log_dev_every(LOG_RATE_NORMAL,
645 "Grid layout: %d clients -> %dx%d grid (%.1f%% utilization) | terminal=%dx%d (char aspect %.2f, VISUAL "
646 "aspect %.2f), video aspect: %.2f",
647 sources_with_video, best_cols, best_rows, best_utilization * 100.0f, terminal_width, terminal_height,
648 (
float)terminal_width / (
float)terminal_height, terminal_visual_aspect, avg_aspect);
662static image_t *create_multi_source_composite(
image_source_t *sources,
int source_count,
int sources_with_video,
663 uint32_t target_client_id,
unsigned short width,
unsigned short height) {
664 (void)target_client_id;
667 int grid_cols, grid_rows;
668 calculate_optimal_grid_layout(sources, source_count, sources_with_video, width, height, &grid_cols, &grid_rows);
674 const int PIXELS_PER_CHAR_HEIGHT = 2;
675 int composite_width_px = width;
676 int composite_height_px = height * PIXELS_PER_CHAR_HEIGHT;
683 int video_source_index = 0;
684 for (
int i = 0; i < source_count && video_source_index < 9; i++) {
685 if (!sources[i].image)
688 int row = video_source_index / grid_cols;
689 int col = video_source_index % grid_cols;
690 video_source_index++;
694 int cell_width_px = composite->w / grid_cols;
695 int cell_height_px = composite->h / grid_rows;
698 float src_aspect = (float)sources[i].image->w / (
float)sources[i].
image->h;
699 float cell_visual_aspect = (float)cell_width_px / (
float)cell_height_px;
701 int target_width_px, target_height_px;
705 if (src_aspect > cell_visual_aspect) {
707 target_width_px = cell_width_px;
708 target_height_px = (int)((cell_width_px / src_aspect) + 0.5f);
711 target_height_px = cell_height_px;
712 target_width_px = (int)((cell_height_px * src_aspect) + 0.5f);
715 log_dev_every(LOG_RATE_NORMAL,
"Cell %d: %dx%d px, video %.1f, cell %.2f → target %dx%d px (fill %s)",
716 video_source_index - 1, cell_width_px, cell_height_px, src_aspect, cell_visual_aspect,
717 target_width_px, target_height_px, (src_aspect > cell_visual_aspect) ?
"WIDTH" :
"HEIGHT");
724 int cell_x_offset_px = col * cell_width_px;
725 int cell_y_offset_px = row * cell_height_px;
733 int x_padding_px, y_padding_px;
737 x_padding_px = (cell_width_px - target_width_px) / 2;
738 y_padding_px = (cell_height_px - target_height_px) / 2;
741 int cell_x_min = cell_x_offset_px;
742 int cell_x_max = cell_x_offset_px + cell_width_px - 1;
743 int cell_y_min = cell_y_offset_px;
744 int cell_y_max = cell_y_offset_px + cell_height_px - 1;
749 for (
int y = 0; y < resized->h; y++) {
750 for (
int x = 0; x < resized->w; x++) {
752 int dst_x = cell_x_offset_px + x_padding_px + x;
753 int dst_y = cell_y_offset_px + y_padding_px + y;
756 if (dst_x < cell_x_min || dst_x > cell_x_max || dst_y < cell_y_min || dst_y > cell_y_max) {
761 if (dst_x < 0 || dst_x >= composite->w || dst_y < 0 || dst_y >= composite->h) {
766 int src_idx = (y * resized->w) + x;
767 int dst_idx = (dst_y * composite->w) + dst_x;
768 composite->pixels[dst_idx] = resized->pixels[src_idx];
787static char *convert_composite_to_ascii(image_t *composite, uint32_t target_client_id,
unsigned short width,
788 unsigned short height) {
795 client_info_t *render_client = NULL;
798 for (
int i = 0; i < MAX_CLIENTS; i++) {
800 if (atomic_load(&client->client_id) == target_client_id) {
801 render_client = client;
806 if (!render_client) {
807 SET_ERRNO(ERROR_INVALID_STATE,
"Per-client %u: Target client not found", target_client_id);
813 bool has_terminal_caps_snapshot = render_client->has_terminal_caps;
814 if (!has_terminal_caps_snapshot) {
815 SET_ERRNO(ERROR_INVALID_STATE,
"Per-client %u: Terminal capabilities not received", target_client_id);
819 terminal_capabilities_t caps_snapshot = render_client->terminal_caps;
821 if (!render_client->client_palette_initialized) {
822 SET_ERRNO(ERROR_TERMINAL,
"Client %u palette not initialized - cannot render frame", target_client_id);
828 const int h = caps_snapshot.render_mode == RENDER_MODE_HALF_BLOCK ? height * 2 : height;
831 log_dev_every(LOG_RATE_SLOW,
"convert_composite_to_ascii: composite=%dx%d, terminal=%dx%d, h=%d (mode=%d)",
832 composite->w, composite->h, width, height, h, caps_snapshot.render_mode);
839 render_client->client_palette_chars);
841 uint64_t convert_duration_ns = convert_end_ns - convert_start_ns;
843 if (convert_duration_ns > 5 * NS_PER_MS_INT) {
844 char duration_str[32];
846 log_warn(
"SLOW_ASCII_CONVERT: Client %u took %s to convert %dx%d image to ASCII", target_client_id, duration_str,
847 composite->w, composite->h);
956 bool wants_stretch,
size_t *out_size,
bool *out_grid_changed,
957 int *out_sources_count) {
963 if (out_grid_changed) {
964 *out_grid_changed =
false;
966 if (out_sources_count) {
967 *out_sources_count = 0;
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,
980 int source_count = collect_video_sources(sources, MAX_CLIENTS);
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++;
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) {
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);
1000 if (out_sources_count) {
1001 *out_sources_count = sources_with_video;
1009 int previous_count = atomic_load(&g_previous_active_video_count);
1010 if (sources_with_video != previous_count) {
1012 if (atomic_compare_exchange_strong(&g_previous_active_video_count, &previous_count, sources_with_video)) {
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;
1024 image_t *composite = NULL;
1026 if (sources_with_video == 0) {
1033 if (sources_with_video == 1) {
1038 image_t *single_source = create_single_source_composite(sources, source_count, target_client_id, width, height);
1039 if (single_source) {
1042 SET_ERRNO(ERROR_MEMORY,
"Failed to copy single source composite");
1050 create_multi_source_composite(sources, source_count, sources_with_video, target_client_id, width, height);
1056 SET_ERRNO(ERROR_INVALID_STATE,
"Per-client %u: Failed to create composite image", target_client_id);
1063 char *ascii_frame = convert_composite_to_ascii(composite, target_client_id, width, height);
1068 size_t ascii_len = strlen(ascii_frame);
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");
1079 const char reset_seq[] =
"\033[0m";
1080 const size_t reset_len = 4;
1082 if (ascii_len >= reset_len) {
1084 const char *frame_end = ascii_frame + ascii_len - reset_len;
1085 if (strncmp(frame_end, reset_seq, reset_len) == 0) {
1087 *out_size = ascii_len;
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) {
1101 *out_size = (size_t)(last_reset - ascii_frame) + reset_len;
1102 ascii_frame[*out_size] =
'\0';
1103 log_warn(
"Frame was missing reset at end (had garbage), truncated from %zu to %zu bytes", ascii_len,
1107 *out_size = ascii_len;
1108 log_warn(
"Frame has no reset sequences, sending full %zu bytes", ascii_len);
1113 *out_size = ascii_len;
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);
1120 if (*out_size >= 50) {
1121 char hex_buf[300] = {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]);
1127 log_dev_every(4500 * US_PER_MS_INT,
"FRAME_LAST_50_BYTES (hex): %s", hex_buf);
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') {
1136 }
else if (last_bytes[i] ==
'\0') {
1142 log_dev_every(4500 * US_PER_MS_INT,
"FRAME_LAST_50_ASCII: %s", ascii_buf);
1147 SET_ERRNO(ERROR_TERMINAL,
"Per-client %u: Failed to convert image to ASCII", target_client_id);
1154 if (composite->alloc_method == IMAGE_ALLOC_POOL) {
1160 for (
int i = 0; i < source_count; i++) {
1161 if (sources[i].image) {
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) {
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);
1286 if (!client || !client->audio_queue || !audio_data || data_size == 0) {
1290 return packet_queue_enqueue(client->audio_queue, PACKET_TYPE_AUDIO_BATCH, audio_data, data_size, 0,
true);
1310 for (
int i = 0; i < MAX_CLIENTS; i++) {
1314 if (atomic_load(&client->client_id) == 0) {
1319 bool is_active = atomic_load(&client->active);
1320 bool is_sending = atomic_load(&client->is_sending_video);
1322 if (is_active && is_sending) {
char * ascii_convert_with_capabilities(image_t *original, const ssize_t width, const ssize_t height, const terminal_capabilities_t *caps, const bool use_aspect_ratio, const bool stretch, const char *palette_chars)
Per-client state management and lifecycle orchestration.
__attribute__((constructor))
Register fork handlers for common module.
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)
atomic_bool g_server_should_exit
Global atomic shutdown flag shared across all threads.
ascii-chat Server Mode Entry Point Header
client_manager_t g_client_manager
Global client manager singleton - central coordination point.
bool any_clients_sending_video(void)
Check if any connected clients are currently sending video.
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.
Multi-client video mixing and ASCII frame generation.
client_info_t clients[MAX_CLIENTS]
Array of client_info_t structures (backing storage)
Image source structure for multi-client video mixing.
uint32_t client_id
Unique client identifier for this source.
bool has_video
Whether this client has active video stream.
image_t * image
Pointer to client's current video frame (owned by buffer system)
asciichat_error_t image_calc_rgb_size(size_t width, size_t height, size_t *out_size)
uint64_t time_get_ns(void)
int format_duration_ns(double nanoseconds, char *buffer, size_t buffer_size)
image_t * image_new_copy(const image_t *source)
void image_destroy_to_pool(image_t *image)
void image_resize(const image_t *s, image_t *d)
void image_clear(image_t *p)
image_t * image_new_from_pool(size_t width, size_t height)
void image_destroy(image_t *p)
asciichat_error_t image_validate_dimensions(size_t width, size_t height)
const video_frame_t * video_frame_get_latest(video_frame_buffer_t *vfb)