ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
pipeline.h File Reference

Three-thread render pipeline: capture → display/encode. More...

Go to the source code of this file.

Typedefs

typedef struct session_capture_ctx session_capture_ctx_t
 
typedef struct session_display_ctx session_display_ctx_t
 
typedef struct session_pipeline_s session_pipeline_t
 

Functions

asciichat_error_t session_pipeline_create (session_capture_ctx_t *capture, session_display_ctx_t *display, session_pipeline_t **out)
 
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)
 
asciichat_error_t session_pipeline_destroy (session_pipeline_t *pipeline)
 

Detailed Description

Three-thread render pipeline: capture → display/encode.

Decouples capture, terminal display, and file encoding into separate threads:

  • Capture thread: reads raw image_t frames from media source
  • Main thread: converts to ASCII, prints to terminal (fast)
  • Encode thread: converts to ASCII, encodes to FFmpeg file (slow, non-blocking)

Definition in file pipeline.h.

Typedef Documentation

◆ session_capture_ctx_t

Definition at line 20 of file pipeline.h.

◆ session_display_ctx_t

Definition at line 21 of file pipeline.h.

◆ session_pipeline_t

Definition at line 22 of file pipeline.h.

Function Documentation

◆ session_pipeline_create()

asciichat_error_t session_pipeline_create ( session_capture_ctx_t *  capture,
session_display_ctx_t *  display,
session_pipeline_t **  out 
)

Create and start the capture thread and (if –render-file) encode thread.

Parameters
captureCapture context (must have real media_source_t for synchronous mode)
displayDisplay context (may have render_file set for –render-file mode)
outPointer to receive allocated pipeline context
Returns
ASCIICHAT_OK on success, error code otherwise
Note
The pipeline does NOT own capture or display — caller must keep them alive until session_pipeline_destroy() returns.

Definition at line 420 of file pipeline.c.

421 {
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}
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INIT
Definition error_codes.h:61
@ ERROR_INVALID_PARAM
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
media_source_type_t media_source_get_type(media_source_t *source)
Get media source type.
Definition source.c:930
@ MEDIA_SOURCE_FILE
Media file (video/audio)
Definition source.h:83
bool session_display_has_render_file(session_display_ctx_t *ctx)
Check if display has render-file configured.
void * session_capture_get_media_source(session_capture_ctx_t *ctx)
Get the underlying media source from capture context.
uint32_t session_display_get_render_fps(session_display_ctx_t *ctx)
Get the render FPS configured for file output.
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)
#define asciichat_thread_join(thread, timeout_ms)
bool media_source_uses_webcam(media_source_t *source)
Definition source.c:519
void session_display_set_render_live_timing(session_display_ctx_t *ctx)
Media source for video and audio capture.
Definition source.c:34
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

References ASCIICHAT_OK, asciichat_thread_create, asciichat_thread_join, atomic_store_bool(), atomic_store_u64(), session_pipeline_s::capture, session_pipeline_s::capture_tid, session_pipeline_s::display, session_pipeline_s::display_queue, session_pipeline_s::encode_queue, session_pipeline_s::encode_tid, ERROR_INIT, ERROR_INVALID_PARAM, session_pipeline_s::first_frame_ns, session_pipeline_s::has_render_file, log_info, MEDIA_SOURCE_FILE, media_source_get_type(), media_source_uses_webcam(), SAFE_CALLOC, SAFE_FREE, session_capture_get_media_source(), session_display_get_render_fps(), session_display_has_render_file(), session_display_set_render_live_timing(), SET_ERRNO, and session_pipeline_s::stop.

Referenced by session_render_loop().

◆ session_pipeline_destroy()

asciichat_error_t session_pipeline_destroy ( session_pipeline_t *  pipeline)

Stop threads and free pipeline.

Waits for capture and encode threads to exit cleanly.

Parameters
pipelinePipeline context (safe to call with NULL)
Returns
ASCIICHAT_OK

Definition at line 557 of file pipeline.c.

557 {
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}
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define NS_PER_MS_INT
Definition time.h:156
int asciichat_thread_join_timeout(asciichat_thread_t *thread, void **retval, uint64_t timeout_ns)
Wait for a thread to complete with timeout.
#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)
bool asciichat_thread_is_initialized(asciichat_thread_t *thread)
Check if a thread handle has been initialized.
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
cond_t not_empty
Definition pipeline.c:44
cond_t not_full
Definition pipeline.c:45
mutex_t mu
Definition pipeline.c:43

References ASCIICHAT_OK, asciichat_thread_is_initialized(), asciichat_thread_join_timeout(), atomic_store_bool(), session_pipeline_s::capture_tid, cond_broadcast(), session_pipeline_s::display_queue, session_pipeline_s::encode_queue, session_pipeline_s::encode_tid, log_info, log_warn, frame_queue_s::mu, mutex_lock, mutex_unlock, frame_queue_s::not_empty, frame_queue_s::not_full, NS_PER_MS_INT, SAFE_FREE, and session_pipeline_s::stop.

Referenced by session_render_loop().

◆ session_pipeline_run_main()

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 
)

Main thread loop: drives terminal output and keyboard handling.

Pops frames from display queue, converts to ASCII, writes to terminal. Handles snapshot delay timer and exit conditions.

Parameters
pipelinePipeline context created by session_pipeline_create()
should_exitCallback to check if rendering should stop
keyboard_handlerOptional keyboard handler callback
user_dataOpaque data passed to callbacks
Returns
ASCIICHAT_OK on success
Note
This function blocks until should_exit() returns true or capture ends. Signals capture/encode threads to stop before returning.

Definition at line 474 of file pipeline.c.

475 {
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}
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
unsigned long long uint64_t
Definition common.h:59
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
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
keyboard_key_t keyboard_read_nonblocking(void)
Read next keyboard input without blocking.
keyboard_key_t
Unified keyboard key code enumeration.
Definition keyboard.h:54
@ KEY_NONE
No key pressed or no input available.
Definition keyboard.h:55
void session_display_write_ascii(session_display_ctx_t *ctx, const char *ascii)
Write ASCII frame to terminal only (no encoding)
bool keyboard_help_is_active(session_display_ctx_t *ctx)
Check if keyboard help is currently active.
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.
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:702
bool should_exit(void)
Definition misc.c:131
int frame
Definition splash.c:99
Image structure.
int w
Image width in pixels (must be > 0)
RGB pixel structure.

References ASCIICHAT_OK, atomic_load_bool(), atomic_store_bool(), session_pipeline_s::capture, session_pipeline_s::display, session_pipeline_s::display_queue, ERROR_INVALID_PARAM, frame, KEY_NONE, keyboard_help_is_active(), keyboard_read_nonblocking(), log_debug, log_debug_every, log_error, log_info, NS_PER_MS_INT, NS_PER_SEC_INT, SAFE_FREE, session_display_convert_to_ascii(), session_display_write_ascii(), SET_ERRNO, should_exit(), session_pipeline_s::stop, time_get_ns(), and image_t::w.

Referenced by session_render_loop().