ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
pipeline.c
Go to the documentation of this file.
1
7#include "pipeline.h"
8#include "capture.h"
9#include "display.h"
10#include "render.h"
16#include <ascii-chat/log/log.h>
18#include <ascii-chat/atomic.h>
19#include <string.h>
20
21/* ============================================================================
22 * External Declarations
23 * ============================================================================ */
24
25// Flag set by display thread when first frame is rendered
27
28/* ============================================================================
29 * Frame Type & Queue
30 * ============================================================================ */
31
32typedef struct {
33 uint8_t *pixels; // SAFE_MALLOC'd copy
34 int w, h;
35 uint64_t captured_ns; // wall-clock timestamp
37
38// Thread-safe bounded queue (ring buffer)
48
49static frame_queue_t *frame_queue_create(int capacity, const char *name) {
50 frame_queue_t *q = SAFE_CALLOC(1, sizeof(*q), frame_queue_t *);
51 q->slots = SAFE_CALLOC(capacity, sizeof(void *), void *);
52 q->capacity = capacity;
53 q->head = q->tail = q->count = 0;
54 q->name = name;
55 mutex_init(&q->mu, name);
56 cond_init(&q->not_empty, "queue_not_empty");
57 cond_init(&q->not_full, "queue_not_full");
58 return q;
59}
60
61static void frame_queue_destroy(frame_queue_t *q) {
62 if (!q)
63 return;
64 mutex_destroy(&q->mu);
67 SAFE_FREE(q->slots);
68 SAFE_FREE(q);
69}
70
71// Push item, timeout_ns=0 means non-blocking (drops if full)
72static bool frame_queue_push(frame_queue_t *q, void *item, uint64_t timeout_ns) {
73 mutex_lock(&q->mu);
74 while (q->count >= q->capacity) {
75 if (timeout_ns == 0) {
76 // Non-blocking: drop the item
77 mutex_unlock(&q->mu);
78 return false;
79 }
80 if (!cond_timedwait(&q->not_full, &q->mu, timeout_ns)) {
81 mutex_unlock(&q->mu);
82 return false; // timeout
83 }
84 }
85 q->slots[q->tail] = item;
86 q->tail = (q->tail + 1) % q->capacity;
87 q->count++;
89 mutex_unlock(&q->mu);
90 return true;
91}
92
93// Pop item, timeout_ns=0 means blocking forever
94static void *frame_queue_pop(frame_queue_t *q, uint64_t timeout_ns) {
95 mutex_lock(&q->mu);
96 while (q->count == 0) {
97 if (timeout_ns == 0) {
98 // Block forever
99 cond_wait(&q->not_empty, &q->mu);
100 } else {
101 if (!cond_timedwait(&q->not_empty, &q->mu, timeout_ns)) {
102 mutex_unlock(&q->mu);
103 return NULL; // timeout
104 }
105 }
106 }
107 void *item = q->slots[q->head];
108 q->head = (q->head + 1) % q->capacity;
109 q->count--;
111 mutex_unlock(&q->mu);
112 return item;
113}
114
115static int frame_queue_count(frame_queue_t *q) {
116 if (!q)
117 return 0;
118 mutex_lock(&q->mu);
119 int count = q->count;
120 mutex_unlock(&q->mu);
121 return count;
122}
123
124static void frame_queue_flush(frame_queue_t *q, void (*free_fn)(void *)) {
125 if (!q)
126 return;
127 mutex_lock(&q->mu);
128 while (q->count > 0) {
129 void *item = q->slots[q->head];
130 q->head = (q->head + 1) % q->capacity;
131 q->count--;
132 if (item && free_fn)
133 free_fn(item);
134 }
135 mutex_unlock(&q->mu);
136}
137
138/* ============================================================================
139 * Frame Helpers
140 * ============================================================================ */
141
142static pipeline_frame_t *make_frame_copy(const image_t *src) {
143 pipeline_frame_t *f = SAFE_CALLOC(1, sizeof(*f), pipeline_frame_t *);
144 f->w = src->w;
145 f->h = src->h;
147 size_t bytes = src->w * src->h * 3;
148 f->pixels = SAFE_MALLOC(bytes, uint8_t *);
149 memcpy(f->pixels, src->pixels, bytes);
150 return f;
151}
152
153static void free_frame(pipeline_frame_t *f) {
154 if (!f)
155 return;
156 SAFE_FREE(f->pixels);
157 SAFE_FREE(f);
158}
159
160// Generic wrapper for frame_queue_flush that matches the expected void (*)(void *) signature
161static void free_frame_generic(void *f) {
162 free_frame((pipeline_frame_t *)f);
163}
164
165// Validate frame dimensions to detect corruption
166static bool frame_is_valid(const pipeline_frame_t *f) {
167 if (!f) {
168 log_error("Frame pointer is NULL");
169 return false;
170 }
171 // Reasonable bounds: width/height should be between 1 and 10000
172 if (f->w <= 0 || f->w > 10000 || f->h <= 0 || f->h > 10000) {
173 log_error("Invalid frame dimensions detected: w=%d, h=%d (likely corrupted)", f->w, f->h);
174 return false;
175 }
176 if (!f->pixels) {
177 log_error("Frame pixels pointer is NULL");
178 return false;
179 }
180 return true;
181}
182
183/* ============================================================================
184 * Pipeline Context
185 * ============================================================================ */
186
201
202/* ============================================================================
203 * Capture Thread
204 * ============================================================================ */
205
206static void *pipeline_capture_thread(void *arg) {
207 session_pipeline_t *pipeline = (session_pipeline_t *)arg;
208 log_info("[PIPELINE_CAPTURE] Starting capture thread");
209
210 bool snapshot_mode = GET_OPTION(snapshot_mode);
211 uint64_t snapshot_frames_captured = 0;
212 uint64_t snapshot_target_frames = 1;
213 media_source_t *capture_source = session_capture_get_media_source(pipeline->capture);
214 media_source_type_t capture_source_type = media_source_get_type(capture_source);
215 bool snapshot_uses_frame_target =
216 (capture_source_type == MEDIA_SOURCE_FILE || capture_source_type == MEDIA_SOURCE_STDIN) &&
217 !media_source_uses_webcam(capture_source);
218 if (snapshot_mode) {
219 // Initialize duration estimate BEFORE encoding frames so PTS scaling works from frame 1
221 double snapshot_delay = GET_OPTION(snapshot_delay);
222 g_snapshot_actual_duration_ms = (uint64_t)(snapshot_delay * 1000.0);
223 if (snapshot_delay > 0.0 && snapshot_uses_frame_target) {
224 double target_frames = snapshot_delay * session_capture_get_target_fps(pipeline->capture);
225 snapshot_target_frames = (uint64_t)(target_frames + 0.999999);
226 }
227 log_info(
228 "[PIPELINE_CAPTURE] Snapshot mode: initialized g_snapshot_actual_duration_ms=%llu ms (snapshot_delay=%.2f)",
229 (unsigned long long)g_snapshot_actual_duration_ms, snapshot_delay);
230 }
231
232 while (!atomic_load_bool(&pipeline->stop)) {
234
235 if (!img) {
236 if (session_capture_at_end(pipeline->capture)) {
237 log_info("[PIPELINE_CAPTURE] End of media reached");
238 // Push EOF sentinels (zero-initialized)
239 pipeline_frame_t *sentinel1 = SAFE_CALLOC(1, sizeof(*sentinel1), pipeline_frame_t *);
240 frame_queue_push(pipeline->display_queue, sentinel1, 10 * NS_PER_MS_INT);
241 if (pipeline->has_render_file) {
242 pipeline_frame_t *sentinel2 = SAFE_CALLOC(1, sizeof(*sentinel2), pipeline_frame_t *);
243 frame_queue_push(pipeline->encode_queue, sentinel2, 10 * NS_PER_MS_INT);
244 }
245 break;
246 }
248 continue;
249 }
250
251 // Respect configured FPS
253
254 // Copy frame
255 pipeline_frame_t *frame = make_frame_copy(img);
256
257 // Record first frame timestamp for snapshot timer
258 uint64_t zero = 0;
259 atomic_cas_u64(&pipeline->first_frame_ns, &zero, frame->captured_ns);
260
261 // Fan out: display queue (fast, can drop), encode queue (slow, keep all)
262 pipeline_frame_t *display_copy = SAFE_MALLOC(sizeof(*display_copy), pipeline_frame_t *);
263 memcpy(display_copy, frame, sizeof(*display_copy));
264 display_copy->pixels = SAFE_MALLOC(frame->w * frame->h * 3, uint8_t *);
265 memcpy(display_copy->pixels, frame->pixels, frame->w * frame->h * 3);
266
267 uint64_t push_time_ns = time_get_ns();
268 log_info("[PIPELINE_CAPTURE_PUSH_DISPLAY] Pushing %dx%d frame to display_queue at %llu ns", display_copy->w,
269 display_copy->h, (unsigned long long)push_time_ns);
270 if (!frame_queue_push(pipeline->display_queue, display_copy, 0)) {
271 // Non-blocking: drop if queue full
272 log_warn("[PIPELINE_CAPTURE_DROP] Display queue full, dropping frame");
273 free_frame(display_copy);
274 }
275
276 if (pipeline->has_render_file) {
277 // Encode all captured frames, including frames before display rendering starts
278 // The encoder will use them with correct timestamps regardless of display timing
279 if (!frame_queue_push(pipeline->encode_queue, frame, 500 * NS_PER_MS_INT)) {
280 // Non-blocking: drop if queue full after 500ms
281 log_warn("[PIPELINE_CAPTURE] Encode queue blocked, dropping frame");
282 free_frame(frame);
283 } else {
284 log_debug_every(60 * NS_PER_SEC_INT, "[PIPELINE_CAPTURE] Enqueued frame to encode_queue (%dx%d)", frame->w,
285 frame->h);
286 }
287 } else {
288 log_warn_every(1000 * NS_PER_MS_INT, "[PIPELINE_CAPTURE] has_render_file=false, NOT encoding frames");
289 free_frame(frame);
290 }
291
292 // Check snapshot mode elapsed time AFTER queueing frame (ensure at least 1 frame is queued)
293 if (snapshot_mode) {
294 snapshot_frames_captured++;
295 uint64_t now_ns = time_get_ns();
296
297 // Set first capture timestamp on first frame (only once)
300 log_info("[SNAPSHOT_CAPTURE] First frame captured at %llu ns", (unsigned long long)now_ns);
301 }
302
303 // Calculate elapsed time from first capture
304 double elapsed = (double)(now_ns - g_snapshot_first_capture_ns) / NS_PER_SEC_INT;
305 double snapshot_delay = GET_OPTION(snapshot_delay);
306 bool snapshot_complete =
307 snapshot_uses_frame_target ? snapshot_frames_captured >= snapshot_target_frames : elapsed >= snapshot_delay;
308
309 if (snapshot_complete) {
310 log_info("[PIPELINE_CAPTURE] Snapshot video elapsed=%.3f reached delay=%.2f - stopping capture but waiting for "
311 "encode queue to drain",
312 elapsed, snapshot_delay);
313
314 // Update with actual measured duration so encoder can scale frames accurately
317 uint64_t actual_ms =
318 snapshot_uses_frame_target ? (uint64_t)(snapshot_delay * 1000.0) : (uint64_t)(elapsed * 1000.0);
320 uint64_t last_frame_elapsed_ns = now_ns - g_snapshot_first_capture_ns;
321 g_snapshot_last_capture_elapsed_ns = last_frame_elapsed_ns;
322 log_info("[PIPELINE_CAPTURE] g_snapshot_actual_duration_ms=%llu, last_capture_elapsed_ns=%llu",
323 (unsigned long long)actual_ms, (unsigned long long)last_frame_elapsed_ns);
324
325 // Wait for encode queue to drain (all buffered frames processed)
326 // This ensures slow encoders can catch up and encode all captured frames
327 // Skip wait if snapshot_delay is 0 (immediate exit after 1 frame)
328 double snapshot_delay = GET_OPTION(snapshot_delay);
329 if (pipeline->has_render_file && snapshot_delay > 0) {
330 while (frame_queue_count(pipeline->encode_queue) > 0) {
331 log_debug("[PIPELINE_CAPTURE] Waiting for encode queue to drain (%d frames pending)",
332 frame_queue_count(pipeline->encode_queue));
334 }
335 log_info("[PIPELINE_CAPTURE] Encode queue drained, sending EOF sentinel");
336 }
337
338 // Push EOF sentinels (zero-initialized)
339 pipeline_frame_t *sentinel1 = SAFE_CALLOC(1, sizeof(*sentinel1), pipeline_frame_t *);
340 frame_queue_push(pipeline->display_queue, sentinel1, 10 * NS_PER_MS_INT);
341 if (pipeline->has_render_file) {
342 pipeline_frame_t *sentinel2 = SAFE_CALLOC(1, sizeof(*sentinel2), pipeline_frame_t *);
343 frame_queue_push(pipeline->encode_queue, sentinel2, 10 * NS_PER_MS_INT);
344 }
345 break;
346 }
347 }
348 }
349
350 log_info("[PIPELINE_CAPTURE] Capture thread exiting");
351 return NULL;
352}
353
354/* ============================================================================
355 * Encode Thread
356 * ============================================================================ */
357
358static void *pipeline_encode_thread(void *arg) {
359 session_pipeline_t *pipeline = (session_pipeline_t *)arg;
360 log_info("[PIPELINE_ENCODE] Starting encode thread");
361
362 uint64_t frames_processed = 0;
363 bool received_eof = false;
364
365 while (!received_eof) {
366 // Pop with indefinite wait while we haven't received EOF
367 // The stop flag does NOT exit the loop - only EOF sentinel does
368 // This ensures all queued frames are processed before thread exits
369 pipeline_frame_t *frame = (pipeline_frame_t *)frame_queue_pop(pipeline->encode_queue, 100 * NS_PER_MS_INT);
370
371 if (!frame) {
372 // Timeout - check if stop flag is set AND queue is empty
373 // Only exit timeout loop if stop flag is true (capture thread has finished)
374 if (atomic_load_bool(&pipeline->stop)) {
375 log_debug("[PIPELINE_ENCODE] Stop flag set and frame pop timed out, continuing to wait for EOF");
376 }
377 log_debug_every(1 * NS_PER_SEC_INT, "[PIPELINE_ENCODE] Waiting for frames (processed=%llu)",
378 (unsigned long long)frames_processed);
379 continue; // timeout, keep waiting
380 }
381
382 if (!frame->pixels) {
383 // EOF sentinel
384 log_info("[PIPELINE_ENCODE] Received EOF sentinel, exiting (processed=%llu frames)",
385 (unsigned long long)frames_processed);
386 free_frame(frame);
387 received_eof = true;
388 break;
389 }
390
391 // Validate frame before processing (catch memory corruption)
392 if (!frame_is_valid(frame)) {
393 log_error("Skipping frame with corrupted dimensions in encode thread");
394 free_frame(frame);
395 continue;
396 }
397
398 if (frames_processed == 0 || frames_processed % 30 == 0) {
399 log_info("[PIPELINE_ENCODE] Processing frame %llu: %dx%d", (unsigned long long)frames_processed, frame->w,
400 frame->h);
401 }
402
403 // Encode frame (convert_to_ascii is called internally by encode_frame)
404 // Avoid double-conversion that was causing state desynchronization between display/encode threads
405 image_t raw_image = {.w = frame->w, .h = frame->h, .pixels = (rgb_pixel_t *)frame->pixels};
406 session_display_encode_frame(pipeline->display, &raw_image, frame->captured_ns);
407 frames_processed++;
408
409 free_frame(frame);
410 }
411
412 log_info("[PIPELINE_ENCODE] Encode thread exiting (total frames encoded=%llu)", (unsigned long long)frames_processed);
413 return NULL;
414}
415
416/* ============================================================================
417 * Public API
418 * ============================================================================ */
419
421 session_pipeline_t **out) {
422 if (!capture || !display || !out) {
423 return SET_ERRNO(ERROR_INVALID_PARAM, "session_pipeline_create: NULL argument");
424 }
425
427 p->capture = capture;
428 p->display = display;
429 p->display_queue = frame_queue_create(4, "display");
430 p->encode_queue = frame_queue_create(256, "encode"); // Large buffer for slow encoding
431
432 // Check if display has render_file configured (will be checked in encode thread)
433 // We store a flag to know whether to enqueue frames for encoding
434 double render_fps = session_display_get_render_fps(display);
435 bool has_file = session_display_has_render_file(display);
439
440 // Enable encode thread if render_file is available
441 // Note: has_file can be true even if render_file creation failed (ctx->render_file exists)
442 // OR if render_fps is explicitly set for playback speed control
443 p->has_render_file = render_fps > 0 || has_file;
444 log_info("[PIPELINE_CREATE] render_fps=%.1f, has_file=%d, has_render_file=%d", render_fps, has_file ? 1 : 0,
445 p->has_render_file ? 1 : 0);
446
447 atomic_store_bool(&p->stop, false);
449
450 // Start capture thread
451 if (asciichat_thread_create(&p->capture_tid, "pipeline_capture", pipeline_capture_thread, p) != 0) {
452 frame_queue_destroy(p->display_queue);
453 frame_queue_destroy(p->encode_queue);
454 SAFE_FREE(p);
455 return SET_ERRNO(ERROR_INIT, "session_pipeline_create: failed to start capture thread");
456 }
457
458 // Start encode thread only if render_file is active
459 if (p->has_render_file) {
460 if (asciichat_thread_create(&p->encode_tid, "pipeline_encode", pipeline_encode_thread, p) != 0) {
461 atomic_store_bool(&p->stop, true);
463 frame_queue_destroy(p->display_queue);
464 frame_queue_destroy(p->encode_queue);
465 SAFE_FREE(p);
466 return SET_ERRNO(ERROR_INIT, "session_pipeline_create: failed to start encode thread");
467 }
468 }
469
470 *out = p;
471 return ASCIICHAT_OK;
472}
473
475 session_keyboard_handler_fn keyboard_handler, void *user_data) {
476 if (!pipeline || !should_exit) {
477 return SET_ERRNO(ERROR_INVALID_PARAM, "session_pipeline_run_main: NULL argument");
478 }
479
480 log_info("[PIPELINE_MAIN] Starting main thread loop");
481
482 // Snapshot duration is owned by the capture thread. It records the first
483 // captured frame, runs until snapshot_delay has elapsed, then sends the EOF
484 // sentinel. The display thread must wait for that sentinel instead of using
485 // a second timer based on terminal rendering speed; large terminals can make
486 // ASCII conversion and render-file encoding substantially slower than capture.
487 while (!should_exit(user_data) && !atomic_load_bool(&pipeline->stop)) {
488 bool exit_check = should_exit(user_data);
489 log_debug_every(NS_PER_SEC_INT, "[PIPELINE_DEBUG] should_exit=%d, stop=%d", exit_check,
490 atomic_load_bool(&pipeline->stop));
491
492 uint64_t pop_time_ns = time_get_ns();
493 pipeline_frame_t *frame = (pipeline_frame_t *)frame_queue_pop(pipeline->display_queue, 1 * NS_PER_MS_INT);
494
495 // Pause stops frame production, so keep polling the keyboard on queue
496 // timeouts or there would be no way to resume playback.
497 if (keyboard_handler) {
499 if (key != KEY_NONE) {
500 log_debug("PIPELINE_KEYBOARD: Received key=%d", key);
501 keyboard_handler(pipeline->capture, (int)key, user_data);
502 }
503 }
504
505 if (!frame)
506 continue; // timeout, check should_exit again
507
508 // EOF sentinels are intentionally zero-initialized and therefore do not
509 // satisfy normal frame validation.
510 if (!frame->pixels) {
511 log_info("[PIPELINE_MAIN_EOF] Received EOF sentinel, stopping");
512 free_frame(frame);
513 frame = NULL;
514 break;
515 }
516
517 // Validate frame structure exists before accessing fields
518 if (!frame_is_valid(frame)) {
519 log_error("Received invalid frame: corrupted or NULL");
520 free_frame(frame);
521 frame = NULL;
522 continue;
523 }
524
525 // Log frame pop with timestamp
526 log_info("[PIPELINE_MAIN_POP] frame=%p, pixels=%p, w=%d, h=%d at %llu ns", (void *)frame, (void *)frame->pixels,
527 frame->w, frame->h, (unsigned long long)pop_time_ns);
528
529 // Check if help screen is active - if so, don't render ASCII frames
530 bool help_is_active = pipeline->display && keyboard_help_is_active(pipeline->display);
531
532 if (!help_is_active) {
533 log_info("[PIPELINE_MAIN_RENDER] Converting frame to ASCII: %dx%d", frame->w, frame->h);
534 // Convert to ASCII
535 image_t tmp = {.w = frame->w, .h = frame->h, .pixels = (rgb_pixel_t *)frame->pixels};
536 char *ascii = session_display_convert_to_ascii(pipeline->display, &tmp);
537 free_frame(frame);
538 frame = NULL;
539
540 if (ascii) {
541 // Write ASCII to terminal
542 session_display_write_ascii(pipeline->display, ascii);
543 SAFE_FREE(ascii);
544 }
545 } else {
546 free_frame(frame);
547 frame = NULL;
548 }
549 }
550
551 log_info("[PIPELINE_MAIN] Main loop exiting, signaling threads to stop");
552 atomic_store_bool(&pipeline->stop, true);
553
554 return ASCIICHAT_OK;
555}
556
558 if (!pipeline)
559 return ASCIICHAT_OK;
560
561 log_info("[PIPELINE] Destroying pipeline, waiting for threads...");
562
563 // Signal threads to stop (may already be stopped)
564 atomic_store_bool(&pipeline->stop, true);
565
566 // Wake up any threads blocked on condition variable waits
567 // so they can check the stop flag and exit immediately
568 // instead of waiting for timeouts (e.g., 100ms frame_queue_pop timeout)
569 if (pipeline->display_queue) {
570 mutex_lock(&pipeline->display_queue->mu);
573 mutex_unlock(&pipeline->display_queue->mu);
574 }
575 if (pipeline->encode_queue) {
576 mutex_lock(&pipeline->encode_queue->mu);
579 mutex_unlock(&pipeline->encode_queue->mu);
580 }
581
582 // Drain and wait for capture thread with 1 second timeout
584 int join_result = asciichat_thread_join_timeout(&pipeline->capture_tid, NULL, 1000 * NS_PER_MS_INT);
585 if (join_result != 0) {
586 log_warn("[PIPELINE] Capture thread join timed out or failed (result=%d)", join_result);
587 }
588 }
589
590 // Drain and wait for encode thread with 1 second timeout
592 int join_result = asciichat_thread_join_timeout(&pipeline->encode_tid, NULL, 1000 * NS_PER_MS_INT);
593 if (join_result != 0) {
594 log_warn("[PIPELINE] Encode thread join timed out or failed (result=%d)", join_result);
595 }
596 }
597
598 // Flush queues to free any remaining frames
599 frame_queue_flush(pipeline->display_queue, free_frame_generic);
600 frame_queue_flush(pipeline->encode_queue, free_frame_generic);
601
602 // Don't free queue structures - debug_sync monitoring thread may still be accessing
603 // the condition variables. Queues will be cleaned up with the pipeline structure.
604 SAFE_FREE(pipeline);
605
606 log_info("[PIPELINE] Pipeline destroyed");
607 return ASCIICHAT_OK;
608}
Platform abstraction layer umbrella header providing unified cross-platform API.
bool atomic_cas_u64(atomic_t *a, uint64_t *expected, uint64_t new_value)
Atomically compare-and-swap a uint64_t.
Definition atomic.c:264
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241
⚛️ Atomic operations abstraction layer with debug tracking
📹 Unified media capture abstraction for session-based video sources
uint64_t g_snapshot_last_capture_elapsed_ns
uint64_t g_snapshot_first_capture_ns
uint64_t g_snapshot_actual_duration_ms
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INIT
Definition error_codes.h:61
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
media_source_type_t
Media source type enumeration.
Definition source.h:81
media_source_type_t media_source_get_type(media_source_t *source)
Get media source type.
Definition source.c:930
@ MEDIA_SOURCE_STDIN
Piped or redirected input.
Definition source.h:84
@ MEDIA_SOURCE_FILE
Media file (video/audio)
Definition source.h:83
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
#define NS_PER_SEC_INT
Definition time.h:157
#define NS_PER_MS_INT
Definition time.h:156
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
int asciichat_thread_join_timeout(asciichat_thread_t *thread, void **retval, uint64_t timeout_ns)
Wait for a thread to complete with timeout.
#define cond_wait(cond, mutex)
Wait on a condition variable (with debug tracking in debug builds)
Definition cond.h:275
keyboard_key_t keyboard_read_nonblocking(void)
Read next keyboard input without blocking.
void platform_sleep_ns(uint64_t ns)
Platform-safe sleep function with nanosecond precision.
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
int cond_broadcast(cond_t *cond)
Broadcast to a condition variable (wake all waiting threads)
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
int cond_signal(cond_t *cond)
Signal a condition variable (wake one waiting thread)
bool asciichat_thread_is_initialized(asciichat_thread_t *thread)
Check if a thread handle has been initialized.
int cond_init(cond_t *cond, const char *name)
Initialize a condition variable with a name.
keyboard_key_t
Unified keyboard key code enumeration.
Definition keyboard.h:54
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
#define cond_timedwait(cond, mutex, timeout_ns)
Wait on a condition variable with timeout (with debug tracking in debug builds)
Definition cond.h:300
int cond_destroy(cond_t *cond)
Destroy a condition variable.
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
@ KEY_NONE
No key pressed or no input available.
Definition keyboard.h:55
void session_display_encode_frame(session_display_ctx_t *ctx, const image_t *image, uint64_t captured_ns)
Encode frame to render-file (FFmpeg only, no terminal output)
void session_display_write_ascii(session_display_ctx_t *ctx, const char *ascii)
Write ASCII frame to terminal only (no encoding)
bool(* session_should_exit_fn)(void *user_data)
Exit condition callback type.
void(* session_keyboard_handler_fn)(session_capture_ctx_t *capture, int key, void *user_data)
Keyboard input handler callback type.
bool keyboard_help_is_active(session_display_ctx_t *ctx)
Check if keyboard help is currently active.
bool session_capture_at_end(session_capture_ctx_t *ctx)
Check if capture source has reached end of stream.
image_t * session_capture_read_frame(session_capture_ctx_t *ctx)
Read the next video frame from the capture source.
bool session_display_has_render_file(session_display_ctx_t *ctx)
Check if display has render-file configured.
uint32_t session_capture_get_target_fps(session_capture_ctx_t *ctx)
Get the target FPS configured for this capture context.
void * session_capture_get_media_source(session_capture_ctx_t *ctx)
Get the underlying media source from capture context.
void session_capture_sleep_for_fps(session_capture_ctx_t *ctx)
Sleep to maintain target frame rate.
char * session_display_convert_to_ascii(session_display_ctx_t *ctx, const image_t *image)
Convert an image to ASCII art using display context and command-line options.
uint32_t session_display_get_render_fps(session_display_ctx_t *ctx)
Get the render FPS configured for file output.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
🎮 Cross-platform keyboard input interface for ascii-chat
🆘 Interactive keyboard help overlay for session keyboard shortcuts
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)
void * asciichat_thread_t
#define asciichat_thread_join(thread, timeout_ms)
📝 Logging API with multiple log levels and terminal output control
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:702
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:708
bool should_exit(void)
Definition misc.c:131
asciichat_error_t session_pipeline_run_main(session_pipeline_t *pipeline, session_should_exit_fn should_exit, session_keyboard_handler_fn keyboard_handler, void *user_data)
Definition pipeline.c:474
struct frame_queue_s frame_queue_t
asciichat_error_t session_pipeline_create(session_capture_ctx_t *capture, session_display_ctx_t *display, session_pipeline_t **out)
Definition pipeline.c:420
bool g_snapshot_first_frame_rendered
asciichat_error_t session_pipeline_destroy(session_pipeline_t *pipeline)
Definition pipeline.c:557
Three-thread render pipeline: capture → display/encode.
Cross-platform memory allocation utilities.
Per-client rendering threads with rate limiting.
bool media_source_uses_webcam(media_source_t *source)
Definition source.c:519
int frame
Definition splash.c:99
void session_display_set_render_live_timing(session_display_ctx_t *ctx)
🖥️ Unified terminal display abstraction for session-based rendering
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
Condition variable type (POSIX: pthread_cond_t with debug tracking)
Definition cond.h:63
void ** slots
Definition pipeline.c:40
const char * name
Definition pipeline.c:46
cond_t not_empty
Definition pipeline.c:44
cond_t not_full
Definition pipeline.c:45
mutex_t mu
Definition pipeline.c:43
Image structure.
int w
Image width in pixels (must be > 0)
int h
Image height in pixels (must be > 0)
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order)
Media source for video and audio capture.
Definition source.c:34
Mutex type (POSIX: pthread_mutex_t with debug tracking)
uint64_t captured_ns
Definition pipeline.c:35
uint8_t * pixels
Definition pipeline.c:33
RGB pixel structure.
Internal session capture context structure.
Internal session display context structure.
asciichat_thread_t encode_tid
Definition pipeline.c:189
asciichat_thread_t capture_tid
Definition pipeline.c:188
frame_queue_t * display_queue
Definition pipeline.c:191
session_display_ctx_t * display
Definition pipeline.c:195
atomic_t first_frame_ns
Definition pipeline.c:198
frame_queue_t * encode_queue
Definition pipeline.c:192
session_capture_ctx_t * capture
Definition pipeline.c:194
⏱️ High-precision timing utilities using sokol_time.h and uthash