ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
client/capture.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define CLIENT_CAPTURE_WEBCAM_INDEX_BUFFER_SIZE 32
4
5#include <ascii-chat/otel.h>
95#include "session/capture.h"
96#include "main.h"
97#include "../main.h" // Global exit API
98#include "server.h"
99#include "audio.h"
100#include "session/capture.h"
103#include <ascii-chat/common.h>
106#include <ascii-chat/options/rcu.h> // For RCU-based options access
107#include <ascii-chat/util/fps.h>
108#include <ascii-chat/util/time.h>
109#include <ascii-chat/util/thread.h> // For THREAD_IS_CREATED macro
112#include <libplatform/atomic.h>
114#include <time.h>
115#include <string.h>
116#include <libplatform/compiler.h>
117#include <libplatform/system.h>
118#include <libplatform/terminal.h>
120
121/* ============================================================================
122 * Session Capture Context
123 * ============================================================================ */
124
134static session_capture_ctx_t *g_capture_capture_ctx = NULL;
135
136/* ============================================================================
137 * Capture Thread Management
138 * ============================================================================ */
139
140/* ============================================================================
141 * Frame Processing Constants
142 * ============================================================================ */
144/* Keep the implicit network cadence below the synchronization inspector's
145 * per-second lock budget. Users can still request a higher rate explicitly
146 * with --fps; the negotiated peer rate remains authoritative once connected. */
147#define CAPTURE_TARGET_FPS 6
148
149/* Frame processing now handled by session library via session_capture_process_for_transmission() */
150/* ============================================================================
151 * Capture Thread Implementation
152 * ============================================================================ */
153
181static void *webcam_capture_thread_func(void *arg) {
182 client_connection_t *connection = arg;
183
184 // FPS tracking for webcam capture thread
185 fps_t fps_tracker = {0};
186 bool fps_tracker_initialized = false;
187 uint64_t capture_frame_count = 0;
188 uint64_t last_capture_frame_time_ns = 0;
189 image_t *transmission_frame = NULL; // Capture worker owns reusable network pixels.
190 image_t *last_frame = NULL; // Borrows transmission_frame for paused playback.
191 const char *video_codec = GET_OPTION(video_codec);
192 bool automatic_codec = strcmp(video_codec, "auto") == 0;
193 video_codec_t selected_codec = strcmp(video_codec, "raw") == 0 ? VIDEO_CODEC_RGBA
194 : strcmp(video_codec, "h264") == 0 ? VIDEO_CODEC_H264
196 if (automatic_codec) {
197 while (selected_codec != VIDEO_CODEC_RGBA && !video_encoder_is_available(selected_codec))
198 selected_codec = selected_codec == VIDEO_CODEC_H265 ? VIDEO_CODEC_H264 : VIDEO_CODEC_RGBA;
199 }
200 // Each connection starts at the requested codec with a fresh bitstream.
202 connection->video_encoder = NULL;
203 if (!fps_tracker_initialized) {
204 int target_fps = (int)session_capture_get_target_fps(connection->capture_context);
205 fps_init(&fps_tracker, target_fps, "WEBCAM_TX");
206 fps_tracker_initialized = true;
207 }
208
209 while (!should_exit() && !server_connection_is_lost(connection)) {
210 // Check connection status
211 if (!server_connection_is_active(connection)) {
212 log_debug_every(LOG_RATE_NORMAL, "Capture thread: waiting for connection to become active");
213 platform_sleep_us(100 * US_PER_MS_INT); // Wait for connection
214 continue;
215 }
216
217 // Frame rate limiting using session capture adaptive sleep
219
220 // Re-check connection after FPS sleep in case it was lost during sleep (reconnection)
221 if (!server_connection_is_active(connection) || server_connection_is_lost(connection)) {
222 log_debug("Capture thread: connection lost after FPS sleep, exiting");
223 break;
224 }
225
226 ASCII_OTEL_SCOPE(CLIENT_VIDEO_FRAME);
227 // Read frame using session capture library
230 asciichat_error_context_t capture_failure;
231 if (!image && errno_peek_since(frame_scope, &capture_failure)) {
232 errno_request_exit(capture_failure.code);
233 signal_exit();
234 break;
235 }
236
237 // Check if media is paused and we have a last frame - render last frame to keep keyboard polling active
238 if (!image) {
241 last_frame) {
242 // Use last frame when paused (keeps display thread rendering and keyboard polling active)
243 image = last_frame;
244 log_debug_every(LOG_RATE_SLOW, "Using cached frame while paused");
245 } else if (session_capture_at_end(connection->capture_context)) {
246 // Check if we've reached end of file for media sources
247 log_debug("Media source reached end of file");
248 last_frame = NULL;
249 break; // Exit capture loop - end of media
250 } else {
251 log_debug_every(LOG_RATE_SLOW, "No frame available from media source yet (returned NULL)");
252 platform_sleep_us(10 * US_PER_MS_INT); // 10ms delay before retry
253 continue;
254 }
255 }
256
257 // Track frame for FPS reporting
258 fps_frame_ns(&fps_tracker, time_get_ns(), "webcam frame captured");
259
260 // Copy/resize into capture-owned storage; the media source retains its input.
261 if (session_capture_process_for_transmission_into(connection->capture_context, image, &transmission_frame) !=
263 continue;
264 image_t *processed_image = transmission_frame;
265
266 // Check connection before sending
267 if (!server_connection_is_active(connection)) {
268 log_warn("Connection lost before sending, stopping video transmission");
269 break;
270 }
271
272 // Send frame packet to server using proper packet format
273 acip_transport_t *transport = server_connection_get_transport(connection);
274 if (!transport || !server_connection_is_active(connection)) {
275 log_warn("Transport became unavailable during capture, stopping transmission");
276 break;
277 }
278
279 uint64_t send_start_ns = time_get_ns();
280
282 asciichat_error_t send_result;
283 bool encoding_failed = false;
284 for (;;) {
285 if (selected_codec == VIDEO_CODEC_RGBA) {
286 send_result = threaded_send_image_frame(connection, processed_image->pixels, (uint32_t)processed_image->w,
287 (uint32_t)processed_image->h, 1);
288 break;
289 }
291 send_result = threaded_send_image_frame_encoded(connection, processed_image->pixels, (uint32_t)processed_image->w,
292 (uint32_t)processed_image->h, selected_codec, &encoding_failed);
293 if (!encoding_failed || !automatic_codec || send_result == ERROR_MEMORY) {
294 if (send_result == ASCIICHAT_OK)
296 break;
297 }
298 log_warn("%s encoding failed; automatic video is switching to %s",
299 selected_codec == VIDEO_CODEC_H265 ? "HEVC" : "H.264",
300 selected_codec == VIDEO_CODEC_H265 ? "H.264" : "raw");
303 connection->video_encoder = NULL;
304 selected_codec = selected_codec == VIDEO_CODEC_H265 ? VIDEO_CODEC_H264 : VIDEO_CODEC_RGBA;
305 encoding_failed = false;
306 }
307 if (send_result == ASCIICHAT_OK)
310 const char *codec_name = selected_codec == VIDEO_CODEC_H265 ? "H265"
311 : selected_codec == VIDEO_CODEC_H264 ? "H264"
312 : "RAW";
313 if (encoding_failed) {
314 log_error("%s video encoding failed (%s). Use --video-codec auto for fallback or --video-codec raw.", codec_name,
315 asciichat_error_format(NULL, send_result));
316 errno_request_exit(send_result);
317 signal_exit();
318 break;
319 }
320
321 // If send failed due to connection loss, break out of loop
322 if (send_result != ASCIICHAT_OK && !server_connection_is_active(connection)) {
323 log_debug("Connection lost during send, stopping transmission");
324 break;
325 }
326 uint64_t send_duration_ns = time_elapsed_ns(send_start_ns, time_get_ns());
327
328 if (send_result != ASCIICHAT_OK && network_error_is_local_rejection(send_result)) {
329 LOG_ERRNO_IF_SET("Dropping locally rejected video frame");
331 continue;
332 }
333 if (send_result != ASCIICHAT_OK) {
334 log_error("🔴 CAPTURE_SEND_FAILED: IMAGE_FRAME_%s send failed %s after %.1fms, closing connection", codec_name,
335 asciichat_error_format(NULL, send_result), (double)send_duration_ns / 1e6);
336 server_connection_lost(connection);
337 break;
338 }
339
340 ASSERT_NO_ERRNO_SINCE(send_scope);
342 if (send_duration_ns > 500 * NS_PER_MS_INT) {
343 log_warn("⚠️ SLOW_FRAME_SEND: %.1fms to send %ux%u %s frame (may indicate full send buffer)",
344 (double)send_duration_ns / 1e6, processed_image->w, processed_image->h, codec_name);
345 }
346
347 log_debug_every(LOG_RATE_NORMAL, "CAPTURE_FRAME_SENT: IMAGE_FRAME_%s delivered to server in %.1fms", codec_name,
348 (double)send_duration_ns / 1e6);
349
350 // Keep the processed workspace itself for pause; no second image copy.
351 last_frame = processed_image;
352
353 // FPS tracking - frame successfully captured and sent
354 capture_frame_count++;
355
356 // Calculate time since last frame for lag detection (using nanosecond precision internally)
357 uint64_t frame_capture_time_ns = time_get_ns();
358
359 uint64_t frame_interval_ns = time_elapsed_ns(last_capture_frame_time_ns, frame_capture_time_ns);
360 last_capture_frame_time_ns = frame_capture_time_ns;
361
362 // Expected frame interval in nanoseconds
363 uint64_t expected_interval_ns =
365 uint64_t lag_threshold_ns = expected_interval_ns + (expected_interval_ns / 2); // 50% over expected
366
367 // Log warning if frame took too long to capture (display in milliseconds for readability)
368 if (capture_frame_count > 1 && frame_interval_ns > lag_threshold_ns) {
369 double late_ms = (double)(frame_interval_ns - expected_interval_ns) / 1e6;
370 double expected_ms = (double)expected_interval_ns / 1e6;
371 double actual_ms = (double)frame_interval_ns / 1e6;
372 double actual_fps = 1e9 / (double)frame_interval_ns;
374 "CLIENT CAPTURE LAG: Frame captured %.1fms late (expected %.1fms, got %.1fms, actual fps: %.1f)",
375 late_ms, expected_ms, actual_ms, actual_fps);
376 }
377
378 // Yield to reduce CPU usage
379 platform_sleep_us(1 * US_PER_MS_INT); // 1ms
380 }
381
382#ifdef DEBUG_THREADS
383 log_debug("Webcam capture thread stopped");
384#endif
385
386 image_destroy(transmission_frame);
387 transmission_frame = NULL;
388 last_frame = NULL;
389
390 log_debug("CAPTURE_THREAD_EXIT: About to mark thread as exited");
391 atomic_store_bool(&connection->capture_thread_exited, true);
392 log_debug("CAPTURE_THREAD_EXIT: Thread marked as exited, cleaning up errno");
393
394 // Clean up thread-local error context before exit
395
396 log_debug("CAPTURE_THREAD_EXIT: Exiting capture thread");
397 return NULL;
398}
399/* ============================================================================
400 * Public Interface Functions
401 * ============================================================================ */
413 // Build capture configuration from options
414 session_capture_config_t config = {0};
415 const char *media_url = GET_OPTION(media_url);
416 const char *media_file = GET_OPTION(media_file);
417 bool media_from_stdin = GET_OPTION(media_from_stdin);
418
419 if (media_url && media_url[0] != '\0') {
420 // Network URL streaming (takes priority over --file)
421 // Don't open webcam when streaming from URL
422 config.type = MEDIA_SOURCE_FILE;
423 config.path = media_url;
424 config.loop = false; // Network URLs cannot be looped
425 log_debug("Using network URL: %s (webcam disabled)", media_url);
426 } else if (media_file && media_file[0] != '\0') {
427 // File or stdin streaming - don't open webcam
428 config.type = media_from_stdin ? MEDIA_SOURCE_STDIN : MEDIA_SOURCE_FILE;
429 config.path = media_file;
430 config.loop = GET_OPTION(media_loop) && !media_from_stdin;
431 log_debug("Using media %s: %s (webcam disabled)", media_from_stdin ? "stdin" : "file", media_file);
432 } else if (GET_OPTION(test_pattern)) {
433 // Test pattern mode - don't open real webcam
434 config.type = MEDIA_SOURCE_TEST;
435 config.path = NULL;
436 log_debug("Using test pattern mode");
437 } else {
438 // Webcam mode (default)
439 static char webcam_index_str[CLIENT_CAPTURE_WEBCAM_INDEX_BUFFER_SIZE];
440 safe_snprintf(webcam_index_str, sizeof(webcam_index_str), "%u", GET_OPTION(webcam_index));
441 config.type = MEDIA_SOURCE_WEBCAM;
442 config.path = webcam_index_str;
443 log_debug("Using webcam device %u", GET_OPTION(webcam_index));
444 }
445 int configured_fps = GET_OPTION(fps);
446 config.target_fps = configured_fps > 0 ? (uint32_t)configured_fps : CAPTURE_TARGET_FPS;
447 config.resize_for_network = true; // Client always resizes for network transmission
448
449 // Configure audio capture with fallback to microphone
450 config.enable_audio = true;
451 config.audio_fallback_to_mic = true;
453
454 // Add seek timestamp if specified
455 config.initial_seek_timestamp = GET_OPTION(media_seek_timestamp);
456
457 // Create capture context using session library
459 g_capture_capture_ctx = session_capture_create(&config);
460 if (!g_capture_capture_ctx) {
461 // Check if there's already an error set (e.g., ERROR_WEBCAM_IN_USE)
463 asciichat_error_t existing_error = errno_peek_since(create_scope, &failure) ? failure.code : ASCIICHAT_OK;
464 log_debug("session_capture_create failed %s", asciichat_error_format(NULL, existing_error));
465 if (existing_error != ASCIICHAT_OK) {
466 log_debug("Returning existing error %s", asciichat_error_format(NULL, existing_error));
467 return existing_error;
468 }
469 SET_ERRNO(ERROR_MEDIA_INIT, "Failed to initialize capture source");
470 return -1;
471 }
472
473 return 0;
474}
475
486 if (!connection->capture_context)
487 return 0;
488 if (THREAD_IS_CREATED(connection->capture_thread_created)) {
489 log_warn("Capture thread already created");
490 return 0;
491 }
492
493 // Start webcam capture thread
494 atomic_store_bool(&connection->capture_thread_exited, false);
495 if (thread_pool_spawn(connection->client_worker_pool, webcam_capture_thread_func, connection, 2, "webcam_capture") !=
496 ASCIICHAT_OK) {
497 SET_ERRNO(ERROR_THREAD, "Webcam capture thread creation failed");
498 LOG_ERRNO_IF_SET("Webcam capture thread creation failed");
499 return -1;
500 }
501
502 connection->capture_thread_created = true;
503 log_debug("Webcam capture thread created successfully");
504
505 return 0;
506}
507
516 if (!connection)
517 return;
518 if (!THREAD_IS_CREATED(connection->capture_thread_created)) {
519 return;
520 }
521
522 // Wait for thread to exit gracefully
523 int wait_count = 0;
524 while (wait_count < 20 && !atomic_load_bool(&connection->capture_thread_exited)) {
525 platform_sleep_us(100 * US_PER_MS_INT); // 100ms
526 wait_count++;
527 }
528
529 if (!atomic_load_bool(&connection->capture_thread_exited)) {
530 log_warn("Capture thread not responding after 2 seconds - will be joined by thread pool");
531 }
532
533 // Thread will be joined by thread_pool_stop_all() in client_connection_stop()
534 connection->capture_thread_created = false;
535}
536
544 return atomic_load_bool(&connection->capture_thread_exited);
545}
546
555
556 // Destroy capture context
557 if (g_capture_capture_ctx) {
558 session_capture_destroy(g_capture_capture_ctx);
559 g_capture_capture_ctx = NULL;
560 }
561}
562
564 return session_capture_get_media_source(g_capture_capture_ctx);
565}
566
568 return g_capture_capture_ctx;
569}
void errno_request_exit(asciichat_error_t code)
bool errno_peek_since(asciichat_errno_scope_t scope, asciichat_error_context_t *out)
void errno_scope_end(asciichat_errno_scope_t scope, asciichat_errno_outcome_t outcome)
asciichat_errno_scope_t errno_checkpoint(void)
asciichat_errno_scope_t errno_scope_begin(void)
const char * asciichat_error_format(const char *message, asciichat_error_t code)
Synchronized per-thread pending error stacks and value snapshots. SET_ERRNO pushes,...
#define SET_ERRNO(code,...)
#define ASSERT_NO_ERRNO_SINCE(scope)
@ ASCIICHAT_ERRNO_HANDLED
@ ASCIICHAT_ERRNO_DISMISSED
#define LOG_ERRNO_IF_SET(message)
void capture_stop_thread(client_connection_t *connection)
Stop capture thread.
bool capture_thread_exited(client_connection_t *connection)
Check if capture thread has exited.
#define CAPTURE_TARGET_FPS
int capture_init()
Initialize capture subsystem.
#define CLIENT_CAPTURE_WEBCAM_INDEX_BUFFER_SIZE
void * capture_get_media_source(void)
void capture_cleanup()
Cleanup capture subsystem.
int capture_start_thread(client_connection_t *connection)
Start capture thread.
session_capture_ctx_t * capture_get_context(void)
ascii-chat Client Mode Entry Point Header
video_codec_t
Definition codecs.h:28
@ VIDEO_CODEC_RGBA
Raw RGBA (4 bytes per pixel, uncompressed).
Definition codecs.h:29
@ VIDEO_CODEC_H264
H.264 (AVC) video codec.
Definition codecs.h:32
@ VIDEO_CODEC_H265
H.265 (HEVC) video codec.
Definition codecs.h:30
void session_capture_complete_open(session_capture_ctx_t *ctx, uint64_t epoch)
uint64_t session_capture_source_epoch(session_capture_ctx_t *ctx)
asciichat_error_t session_capture_process_for_transmission_into(session_capture_ctx_t *ctx, image_t *frame, image_t **output)
📹 Unified media capture abstraction for session-based video sources
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Named object registry for debugging — log identifiable resource names.
bool video_encoder_is_available(video_codec_t codec)
Definition encoder.c:54
void h265_encoder_destroy(h265_encoder_t *encoder)
Definition encoder.c:179
audio_visualization_source_t source
Definition fft.c:24
⏱️ FPS tracking utility for monitoring frame throughput across all threads
unsigned int uint32_t
Definition common.h:62
void fps_frame_ns(fps_t *tracker, uint64_t current_time_ns, const char *context)
Track a frame and detect lag conditions (nanosecond version - PRIMARY).
Definition fps.c:52
unsigned long long uint64_t
Definition common.h:63
void fps_init(fps_t *tracker, int expected_fps, const char *name)
Initialize FPS tracker.
Definition fps.c:32
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_MEDIA_INIT
Definition error_codes.h:65
@ ERROR_THREAD
#define LOG_RATE_FAST
Log rate limit: 1 second (1,000,000 microseconds).
Definition log_rates.h:26
#define LOG_RATE_SLOW
Log rate limit: 10 seconds (10,000,000 microseconds).
Definition log_rates.h:35
#define LOG_RATE_NORMAL
Log rate limit: 3 seconds (3,000,000 microseconds).
Definition log_rates.h:29
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
bool media_source_is_paused(media_source_t *source)
Check if media source is paused.
Definition source.c:1088
media_source_type_t media_source_get_type(media_source_t *source)
Get media source type.
Definition source.c:999
@ MEDIA_SOURCE_STDIN
Piped or redirected input.
Definition source.h:100
@ MEDIA_SOURCE_WEBCAM
Hardware webcam device.
Definition source.h:98
@ MEDIA_SOURCE_FILE
Media file (video/audio).
Definition source.h:99
@ MEDIA_SOURCE_TEST
Test pattern generator.
Definition source.h:101
#define NS_PER_SEC_INT
Definition time.h:169
#define NS_PER_MS_INT
Definition time.h:168
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition time.c:123
#define US_PER_MS_INT
Definition time.h:172
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
bool session_capture_at_end(session_capture_ctx_t *ctx)
Check if capture source has reached end of stream.
struct session_capture_ctx session_capture_ctx_t
session capture config keeps the values that move through the subsystem: type, path,...
image_t * session_capture_read_frame(session_capture_ctx_t *ctx)
Read the next video frame from the capture source.
uint32_t session_capture_get_target_fps(session_capture_ctx_t *ctx)
Get the target FPS configured for this capture context.
struct acip_transport acip_transport_t
Forward declaration of ACIP transport.
Definition host.h:506
session_capture_ctx_t * session_capture_create(const session_capture_config_t *config)
Legacy function - creates either mirror or network capture based on options.
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.
void session_capture_destroy(session_capture_ctx_t *ctx)
Destroy session capture context and free resources.
void image_destroy(image_t *image)
Destroy an image allocated with image_new().
ACIP client-side protocol API.
Network error handling utilities.
bool network_error_is_local_rejection(asciichat_error_t code)
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:749
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:755
void signal_exit(void)
Definition main.c:253
bool should_exit(void)
Definition main.c:249
Global application exit and signal handling API.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
ACIP shared/bidirectional packet sending functions.
🎬 Unified media source abstraction for webcam, files, and stdin
audio_context_t * audio_get_context(void)
Get the global audio context for use by other subsystems.
ascii-chat Client Audio Processing Management Interface
struct client_connection client_connection_t
asciichat_error_t threaded_send_image_frame_encoded(client_connection_t *connection, const void *pixel_data, uint32_t width, uint32_t height, video_codec_t codec, bool *encoding_failed)
Send H.265-encoded video frame to server (thread-safe).
bool server_connection_is_active(client_connection_t *connection)
Establish connection to ascii-chat server.
void server_connection_lost(client_connection_t *connection)
Signal that connection has been lost.
bool server_connection_is_lost(client_connection_t *connection)
Check if connection loss has been detected.
acip_transport_t * server_connection_get_transport(client_connection_t *connection)
Get ACIP transport instance.
asciichat_error_t threaded_send_image_frame(client_connection_t *connection, const void *pixel_data, uint32_t width, uint32_t height, uint32_t pixel_format)
Thread-safe image frame transmission.
ascii-chat Client Server Connection Management Interface
🔤 String Manipulation and Shell Escaping Utilities
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...
thread_pool_t * client_worker_pool
session_capture_ctx_t * capture_context
h265_encoder_t *volatile video_encoder
Counters and timestamps used to calculate and report frame rate.
Definition fps.h:53
SIMD-friendly storage for one RGB pixel. r, g, and b hold the color channels and padding keeps the la...
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:47
Media-source and loader settings used to create a session capture context.
media_source_type_t type
Media source type (WEBCAM, FILE, STDIN, TEST).
const char * path
Device index (for webcam) or file path (for file sources).
uint32_t target_fps
Target frame rate in FPS (e.g., 60 for display, 144 for network).
double initial_seek_timestamp
Seek to this timestamp after opening media (0.0 = no seek).
bool audio_fallback_to_mic
Fall back to microphone if file audio is not available.
bool resize_for_network
Resize frames to network-optimal dimensions (MAX_FRAME_WIDTH x MAX_FRAME_HEIGHT).
bool loop
Enable loop playback for file sources.
bool enable_audio
Enable audio capture from media source.
void * mic_audio_ctx
Microphone audio context for fallback (borrowed, not owned).
test pattern is intentionally a small, concrete record. Fields such as image, rgba give callers the i...
asciichat_error_t thread_pool_spawn(thread_pool_t *pool, void *(*thread_func)(void *), void *thread_arg, int stop_id, const char *thread_name)
Spawn a worker thread in the pool.
🧵 Generic thread pool abstraction for managing worker threads
⏱️ High-precision timing utilities using sokol_time.h and uthash
🧵 Thread lifecycle management helpers
#define THREAD_IS_CREATED(created_flag)
Definition util/thread.h:62