ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
client/capture.c
Go to the documentation of this file.
1
88#include "session/capture.h"
89#include "main.h"
90#include "../main.h" // Global exit API
91#include "server.h"
92#include "audio.h"
93#include "session/capture.h"
96#include <ascii-chat/common.h>
99#include <ascii-chat/options/rcu.h> // For RCU-based options access
100#include <ascii-chat/util/fps.h>
101#include <ascii-chat/util/time.h>
102#include <ascii-chat/util/thread.h> // For THREAD_IS_CREATED macro
105#include <ascii-chat/atomic.h>
107#include <time.h>
108#include <string.h>
111
112/* ============================================================================
113 * Session Capture Context
114 * ============================================================================ */
115
125static session_capture_ctx_t *g_capture_capture_ctx = NULL;
126
127/* ============================================================================
128 * Capture Thread Management
129 * ============================================================================ */
130
139static bool g_capture_thread_created = false;
140
149static atomic_t g_capture_thread_exited = {0};
150
151/* ============================================================================
152 * Frame Processing Constants
153 * ============================================================================ */
155#define CAPTURE_TARGET_FPS 144
156
157/* Frame processing now handled by session library via session_capture_process_for_transmission() */
158/* ============================================================================
159 * Capture Thread Implementation
160 * ============================================================================ */
161
189static void *webcam_capture_thread_func(void *arg) {
190 (void)arg;
191
192 // FPS tracking for webcam capture thread
193 static fps_t fps_tracker = {0};
194 static bool fps_tracker_initialized = false;
195 static uint64_t capture_frame_count = 0;
196 static uint64_t last_capture_frame_time_ns = 0;
197 static image_t *last_frame = NULL; // Cache last frame to render when paused
198 bool force_raw_video = false;
199 if (!fps_tracker_initialized) {
200 fps_init(&fps_tracker, CAPTURE_TARGET_FPS, "WEBCAM_TX");
201 fps_tracker_initialized = true;
202 }
203
204 while (!should_exit() && !server_connection_is_lost()) {
205 // Check connection status
207 log_debug_every(LOG_RATE_NORMAL, "Capture thread: waiting for connection to become active");
208 platform_sleep_us(100 * US_PER_MS_INT); // Wait for connection
209 continue;
210 }
211
212 // Frame rate limiting using session capture adaptive sleep
213 session_capture_sleep_for_fps(g_capture_capture_ctx);
214
215 // Re-check connection after FPS sleep in case it was lost during sleep (reconnection)
217 log_debug("Capture thread: connection lost after FPS sleep, exiting");
218 break;
219 }
220
221 // Read frame using session capture library
222 image_t *image = session_capture_read_frame(g_capture_capture_ctx);
223
224 // Check if media is paused and we have a last frame - render last frame to keep keyboard polling active
225 if (!image) {
226 media_source_t *source = (media_source_t *)session_capture_get_media_source(g_capture_capture_ctx);
227 if (source && media_source_get_type(source) == MEDIA_SOURCE_FILE && media_source_is_paused(source) &&
228 last_frame) {
229 // Use last frame when paused (keeps display thread rendering and keyboard polling active)
230 image = last_frame;
231 log_debug_every(LOG_RATE_SLOW, "Using cached frame while paused");
232 } else if (session_capture_at_end(g_capture_capture_ctx)) {
233 // Check if we've reached end of file for media sources
234 log_debug("Media source reached end of file");
235 image_destroy(last_frame);
236 last_frame = NULL;
237 break; // Exit capture loop - end of media
238 } else {
239 log_debug_every(LOG_RATE_SLOW, "No frame available from media source yet (returned NULL)");
240 platform_sleep_us(10 * US_PER_MS_INT); // 10ms delay before retry
241 continue;
242 }
243 }
244
245 // Track frame for FPS reporting
246 fps_frame_ns(&fps_tracker, time_get_ns(), "webcam frame captured");
247
248 // Process frame for network transmission using session library
249 // session_capture_process_for_transmission() returns a new image that we own
250 // NOTE: The original 'image' is owned by media_source - do NOT free it!
251 image_t *processed_image = session_capture_process_for_transmission(g_capture_capture_ctx, image);
252 if (!processed_image) {
253 SET_ERRNO(ERROR_INVALID_STATE, "Failed to process frame for transmission");
254 // NOTE: Do NOT free 'image' - it's owned by capture context
255 continue;
256 }
257
258 // Check connection before sending
260 log_warn("Connection lost before sending, stopping video transmission");
261 image_destroy(processed_image);
262 break;
263 }
264
265 // Send frame packet to server using proper packet format
267 if (!transport || !server_connection_is_active()) {
268 log_warn("Transport became unavailable during capture, stopping transmission");
269 image_destroy(processed_image);
270 break;
271 }
272
273 uint64_t send_start_ns = time_get_ns();
274
275 // Determine which codec to use based on --video-codec option
276 const char *video_codec = GET_OPTION(video_codec);
277 bool use_hevc = !force_raw_video && video_codec && strcmp(video_codec, "raw") != 0;
278
279 // Request keyframe periodically to force encoder flush (every 30 frames)
280 // This ensures we get some encoded output even when the encoder is buffering inter-frames
281 static uint32_t keyframe_interval = 30;
282 static uint32_t frames_since_keyframe = 0;
283 if (use_hevc && (frames_since_keyframe++ >= keyframe_interval)) {
285 frames_since_keyframe = 0;
286 log_debug("Requesting H.265 keyframe for encoder flush");
287 }
288
289 asciichat_error_t send_result;
290 if (use_hevc) {
291 log_debug_every(LOG_RATE_SLOW, "Capture thread: sending IMAGE_FRAME_H265 %ux%u", processed_image->w,
292 processed_image->h);
293 send_result = threaded_send_image_frame_h265((const void *)processed_image->pixels, (uint32_t)processed_image->w,
294 (uint32_t)processed_image->h);
295 if (send_result == ERROR_MEDIA_INIT) {
296 // Missing platform HEVC encoders should degrade video quality, not tear
297 // down an otherwise healthy audio/video connection.
298 log_warn("HEVC encoding is unavailable; switching this connection to raw video frames");
299 force_raw_video = true;
300 send_result = threaded_send_image_frame((const void *)processed_image->pixels, (uint32_t)processed_image->w,
301 (uint32_t)processed_image->h, 1);
302 }
303 } else {
304 log_debug_every(LOG_RATE_SLOW, "Capture thread: sending IMAGE_FRAME (raw) %ux%u", processed_image->w,
305 processed_image->h);
306 send_result = threaded_send_image_frame((const void *)processed_image->pixels, (uint32_t)processed_image->w,
307 (uint32_t)processed_image->h, 1); // pixel_format = 1 (RGB24)
308 }
309
310 // If send failed due to connection loss, break out of loop
311 if (send_result != ASCIICHAT_OK && !server_connection_is_active()) {
312 log_debug("Connection lost during send, stopping transmission");
313 image_destroy(processed_image);
314 break;
315 }
316 uint64_t send_duration_ns = time_elapsed_ns(send_start_ns, time_get_ns());
317
318 if (send_result != ASCIICHAT_OK) {
319 const char *codec_name = use_hevc ? "H265" : "RAW";
320 log_error("🔴 CAPTURE_SEND_FAILED: IMAGE_FRAME_%s send error=%d (%s) after %.1fms, closing connection",
321 codec_name, send_result, asciichat_error_string(send_result), (double)send_duration_ns / 1e6);
323 image_destroy(processed_image);
324 break;
325 }
326
327 if (send_duration_ns > 500 * NS_PER_MS_INT) {
328 const char *codec_name = use_hevc ? "H.265" : "RAW";
329 log_warn("⚠️ SLOW_FRAME_SEND: %.1fms to send %ux%u %s frame (may indicate full send buffer)",
330 (double)send_duration_ns / 1e6, processed_image->w, processed_image->h, codec_name);
331 }
332
333 const char *codec_name = use_hevc ? "H265" : "RAW";
334 log_debug_every(LOG_RATE_NORMAL, "CAPTURE_FRAME_SENT: IMAGE_FRAME_%s delivered to server in %.1fms", codec_name,
335 (double)send_duration_ns / 1e6);
336
337 // Cache last frame for rendering when paused
338 // Make a copy since the original is owned by media_source
339 if (last_frame) {
340 image_destroy(last_frame);
341 }
342 last_frame = image_new(processed_image->w, processed_image->h);
343 if (last_frame) {
344 memcpy(last_frame->pixels, processed_image->pixels,
345 (size_t)processed_image->w * (size_t)processed_image->h * sizeof(rgb_pixel_t));
346 }
347
348 // FPS tracking - frame successfully captured and sent
349 capture_frame_count++;
350
351 // Calculate time since last frame for lag detection (using nanosecond precision internally)
352 uint64_t frame_capture_time_ns = time_get_ns();
353
354 uint64_t frame_interval_ns = time_elapsed_ns(last_capture_frame_time_ns, frame_capture_time_ns);
355 last_capture_frame_time_ns = frame_capture_time_ns;
356
357 // Expected frame interval in nanoseconds
358 uint64_t expected_interval_ns = NS_PER_SEC_INT / (uint64_t)session_capture_get_target_fps(g_capture_capture_ctx);
359 uint64_t lag_threshold_ns = expected_interval_ns + (expected_interval_ns / 2); // 50% over expected
360
361 // Log warning if frame took too long to capture (display in milliseconds for readability)
362 if (capture_frame_count > 1 && frame_interval_ns > lag_threshold_ns) {
363 double late_ms = (double)(frame_interval_ns - expected_interval_ns) / 1e6;
364 double expected_ms = (double)expected_interval_ns / 1e6;
365 double actual_ms = (double)frame_interval_ns / 1e6;
366 double actual_fps = 1e9 / (double)frame_interval_ns;
368 "CLIENT CAPTURE LAG: Frame captured %.1fms late (expected %.1fms, got %.1fms, actual fps: %.1f)",
369 late_ms, expected_ms, actual_ms, actual_fps);
370 }
371
372 // Clean up processed frame
373 image_destroy(processed_image);
374 processed_image = NULL;
375
376 // Yield to reduce CPU usage
378 }
379
380#ifdef DEBUG_THREADS
381 log_debug("Webcam capture thread stopped");
382#endif
383
384 // Clean up cached frame before thread exit
385 if (last_frame) {
386 image_destroy(last_frame);
387 last_frame = NULL;
388 }
389
390 log_debug("CAPTURE_THREAD_EXIT: About to mark thread as exited");
391 atomic_store_bool(&g_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
396
397 log_debug("CAPTURE_THREAD_EXIT: Exiting capture thread");
398 return NULL;
399}
400/* ============================================================================
401 * Public Interface Functions
402 * ============================================================================ */
414 // Build capture configuration from options
415 session_capture_config_t config = {0};
416 const char *media_url = GET_OPTION(media_url);
417 const char *media_file = GET_OPTION(media_file);
418 bool media_from_stdin = GET_OPTION(media_from_stdin);
419
420 if (media_url && media_url[0] != '\0') {
421 // Network URL streaming (takes priority over --file)
422 // Don't open webcam when streaming from URL
423 config.type = MEDIA_SOURCE_FILE;
424 config.path = media_url;
425 config.loop = false; // Network URLs cannot be looped
426 log_debug("Using network URL: %s (webcam disabled)", media_url);
427 } else if (media_file && media_file[0] != '\0') {
428 // File or stdin streaming - don't open webcam
429 config.type = media_from_stdin ? MEDIA_SOURCE_STDIN : MEDIA_SOURCE_FILE;
430 config.path = media_file;
431 config.loop = GET_OPTION(media_loop) && !media_from_stdin;
432 log_debug("Using media %s: %s (webcam disabled)", media_from_stdin ? "stdin" : "file", media_file);
433 } else if (GET_OPTION(test_pattern)) {
434 // Test pattern mode - don't open real webcam
435 config.type = MEDIA_SOURCE_TEST;
436 config.path = NULL;
437 log_debug("Using test pattern mode");
438 } else {
439 // Webcam mode (default)
440 static char webcam_index_str[32];
441 safe_snprintf(webcam_index_str, sizeof(webcam_index_str), "%u", GET_OPTION(webcam_index));
442 config.type = MEDIA_SOURCE_WEBCAM;
443 config.path = webcam_index_str;
444 log_debug("Using webcam device %u", GET_OPTION(webcam_index));
445 }
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
458 g_capture_capture_ctx = session_capture_create(&config);
459 if (!g_capture_capture_ctx) {
460 // Check if there's already an error set (e.g., ERROR_WEBCAM_IN_USE)
461 asciichat_error_t existing_error = GET_ERRNO();
462 log_debug("session_capture_create failed, GET_ERRNO() returned: %d", existing_error);
463 if (existing_error != ASCIICHAT_OK) {
464 log_debug("Returning existing error code %d", existing_error);
465 return existing_error;
466 }
467 SET_ERRNO(ERROR_MEDIA_INIT, "Failed to initialize capture source");
468 return -1;
469 }
470
471 return 0;
472}
484 if (THREAD_IS_CREATED(g_capture_thread_created)) {
485 log_warn("Capture thread already created");
486 return 0;
487 }
488
489 // Register capture atomics with named debug registry
490 static bool capture_atomics_registered = false;
491 if (!capture_atomics_registered) {
492 NAMED_REGISTER_ATOMIC(&g_capture_thread_exited, "capture_webcam_thread_exit_confirmation", NULL);
493 capture_atomics_registered = true;
494 }
495
496 // Start webcam capture thread
497 atomic_store_bool(&g_capture_thread_exited, false);
498 if (thread_pool_spawn(g_client_worker_pool, webcam_capture_thread_func, NULL, 2, "webcam_capture") != ASCIICHAT_OK) {
499 SET_ERRNO(ERROR_THREAD, "Webcam capture thread creation failed");
500 LOG_ERRNO_IF_SET("Webcam capture thread creation failed");
501 return -1;
502 }
503
504 g_capture_thread_created = true;
505 log_debug("Webcam capture thread created successfully");
506
507 return 0;
508}
518 if (!THREAD_IS_CREATED(g_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(&g_capture_thread_exited)) {
525 platform_sleep_us(100 * US_PER_MS_INT); // 100ms
526 wait_count++;
527 }
528
529 if (!atomic_load_bool(&g_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 protocol_stop_connection()
534 g_capture_thread_created = false;
535}
544 return atomic_load_bool(&g_capture_thread_exited);
545}
556
557 // Destroy capture context
558 if (g_capture_capture_ctx) {
559 session_capture_destroy(g_capture_capture_ctx);
560 g_capture_capture_ctx = NULL;
561 }
562}
563
565 return session_capture_get_media_source(g_capture_capture_ctx);
566}
Platform abstraction layer umbrella header providing unified cross-platform API.
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
#define LOG_ERRNO_IF_SET(message)
Check if any error occurred and log it if so.
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
⚛️ Atomic operations abstraction layer with debug tracking
#define CAPTURE_TARGET_FPS
void * capture_get_media_source(void)
thread_pool_t * g_client_worker_pool
Global client worker thread pool.
h265_encoder_t *volatile g_h265_encoder
Global H.265 encoder context for client video streaming.
📹 Unified media capture abstraction for session-based video sources
⚙️ Common definitions, error codes, macros, and types shared throughout the application
asciichat_error_t thread_pool_spawn(void *pool, void *(*thread_func)(void *), void *thread_arg, int stop_id, const char *name)
Spawn a worker thread in the pool.
void h265_encoder_request_keyframe(h265_encoder_t *encoder)
Named object registry for debugging — log identifiable resource names.
⏱️ FPS tracking utility for monitoring frame throughput across all threads
audio_context_t * audio_get_context(void)
Get the global audio context for use by other subsystems.
int capture_start_thread()
Start capture thread.
int capture_init()
Initialize capture subsystem.
void capture_cleanup()
Cleanup capture subsystem.
void capture_stop_thread()
Stop capture thread.
bool capture_thread_exited()
Check if capture thread has exited.
bool server_connection_is_active()
Check if server connection is currently active.
asciichat_error_t threaded_send_image_frame(const void *pixel_data, uint32_t width, uint32_t height, uint32_t pixel_format)
Thread-safe image frame transmission.
acip_transport_t * server_connection_get_transport(void)
Get ACIP transport instance.
bool server_connection_is_lost()
Check if connection loss has been detected.
asciichat_error_t threaded_send_image_frame_h265(const void *pixel_data, uint32_t width, uint32_t height)
Send H.265-encoded video frame to server (thread-safe)
void server_connection_lost()
Signal that connection has been lost.
unsigned int uint32_t
Definition common.h:58
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:59
void fps_init(fps_t *tracker, int expected_fps, const char *name)
Initialize FPS tracker.
Definition fps.c:32
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t with automatic format specifier.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
#define GET_ERRNO()
Get current error code (0 if no error)
void asciichat_errno_destroy(void)
Cleanup error system resources.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_INVALID_STATE
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_MEDIA_INIT
Definition error_codes.h:70
@ 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:574
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
bool media_source_is_paused(media_source_t *source)
Check if media source is paused.
Definition source.c:1022
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_WEBCAM
Hardware webcam device.
Definition source.h:82
@ MEDIA_SOURCE_FILE
Media file (video/audio)
Definition source.h:83
@ MEDIA_SOURCE_TEST
Test pattern generator.
Definition source.h:85
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
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition util/time.c:150
#define US_PER_MS_INT
Definition time.h:160
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
void platform_sleep_us(unsigned int us)
High-precision sleep function with microsecond precision.
image_t * session_capture_process_for_transmission(session_capture_ctx_t *ctx, image_t *frame)
Process a frame for network transmission (resize if needed)
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.
uint32_t session_capture_get_target_fps(session_capture_ctx_t *ctx)
Get the target FPS configured for this capture context.
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 *p)
Destroy an image allocated with image_new()
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
ACIP client-side protocol API.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
🧵 Thread lifecycle management helpers
#define THREAD_IS_CREATED(created_flag)
#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
ACIP shared/bidirectional packet sending functions.
ascii-chat Server Mode Entry Point Header
🎬 Unified media source abstraction for webcam, files, and stdin
🔊 Session-level audio coordination wrapper
Server-side H.265 decoding pipeline for multi-client streams.
Transport instance structure.
Definition transport.h:214
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
FPS tracking state.
Definition fps.h:52
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
RGB pixel structure.
Configuration for 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)
Internal session capture context structure.
🧵 Generic thread pool abstraction for managing worker threads
⏱️ High-precision timing utilities using sokol_time.h and uthash