ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
capture.c
Go to the documentation of this file.
1
88#include "capture.h"
89#include "main.h"
90#include "server.h"
91#include "video/webcam/webcam.h"
92#include "video/image.h"
93#include "common.h"
94#include "util/endian.h"
95#include "asciichat_errno.h"
96#include "options/options.h"
97#include "options/rcu.h" // For RCU-based options access
98#include "util/time.h"
99#include "util/format.h"
100#include "util/image.h"
101#include "util/endian.h"
102#include "util/thread.h"
103#include "util/endian.h"
104#include "util/fps.h"
105#include "network/acip/send.h"
106#include "network/acip/client.h"
107#include <stdatomic.h>
108#include <time.h>
109#include <string.h>
110#include "platform/abstraction.h"
111#include "thread_pool.h"
112
113/* ============================================================================
114 * Capture Thread Management
115 * ============================================================================ */
116
125__attribute__((unused)) static asciichat_thread_t g_capture_thread;
126
135static bool g_capture_thread_created = false;
136
145static atomic_bool g_capture_thread_exited = false;
146
147/* ============================================================================
148 * Frame Processing Constants
149 * ============================================================================ */
151// Use FRAME_INTERVAL_MS from common.h
153#define MAX_FRAME_WIDTH 480
155#define MAX_FRAME_HEIGHT 270
156
157/* ============================================================================
158 * Frame Processing Functions
159 * ============================================================================ */
183static void calculate_optimal_dimensions(ssize_t original_width, ssize_t original_height, ssize_t max_width,
184 ssize_t max_height, ssize_t *result_width, ssize_t *result_height) {
185 // Calculate original aspect ratio
186 float img_aspect = (float)original_width / (float)original_height;
187 // Check if image needs resizing
188 if (original_width <= max_width && original_height <= max_height) {
189 // Image is already within bounds - use as-is
190 *result_width = original_width;
191 *result_height = original_height;
192 return;
193 }
194 // Determine scaling factor based on which dimension is the limiting factor
195 if ((float)max_width / (float)max_height > img_aspect) {
196 // Max box is wider than image aspect - scale by height
197 *result_height = max_height;
198 *result_width = (ssize_t)(max_height * img_aspect);
199 } else {
200 // Max box is taller than image aspect - scale by width
201 *result_width = max_width;
202 *result_height = (ssize_t)(max_width / img_aspect);
203 }
204}
223static image_t *process_frame_for_transmission(image_t *original_image, ssize_t max_width, ssize_t max_height) {
224 if (!original_image) {
225 return NULL;
226 }
227 // Calculate optimal dimensions
228 ssize_t resized_width, resized_height;
229 calculate_optimal_dimensions(original_image->w, original_image->h, max_width, max_height, &resized_width,
230 &resized_height);
231 // Check if resizing is needed
232 if (original_image->w == resized_width && original_image->h == resized_height) {
233 // No resizing needed - create a copy to preserve original
234 image_t *copy = image_new(original_image->w, original_image->h);
235 if (!copy) {
236 SET_ERRNO(ERROR_MEMORY, "Failed to allocate image copy");
237 return NULL;
238 }
239 // Copy pixel data
240 memcpy(copy->pixels, original_image->pixels, (size_t)original_image->w * original_image->h * sizeof(rgb_pixel_t));
241 return copy;
242 }
243 // Create new image for resized frame
244 image_t *resized = image_new(resized_width, resized_height);
245 if (!resized) {
246 SET_ERRNO(ERROR_MEMORY, "Failed to allocate resized image buffer");
247 // Do not destroy original_image - caller owns it
248 return NULL;
249 }
250
251 // Perform resizing operation
252 image_resize(original_image, resized);
253
254 // Return resized image without destroying original
255 // Caller is responsible for destroying both input and output
256 return resized;
257}
258/* ============================================================================
259 * Capture Thread Implementation
260 * ============================================================================ */
261
289static void *webcam_capture_thread_func(void *arg) {
290 (void)arg;
291 struct timespec last_capture_time = {0, 0};
292
293 // FPS tracking for webcam capture thread
294 static fps_t fps_tracker = {0};
295 static bool fps_tracker_initialized = false;
296 static uint64_t capture_frame_count = 0;
297 static struct timespec last_capture_frame_time = {0, 0};
298 static const uint32_t expected_capture_fps = 144;
299 if (!fps_tracker_initialized) {
300 fps_init(&fps_tracker, 144, "WEBCAM_TX");
301 fps_tracker_initialized = true;
302 }
303
304 while (!should_exit() && !server_connection_is_lost()) {
305 // Check connection status
307 platform_sleep_usec(100 * 1000); // Wait for connection
308 continue;
309 }
310
311 // Frame rate limiting using monotonic clock
312 // Always capture at 144fps to support high-refresh displays, regardless of client's rendering FPS
313 const long CAPTURE_INTERVAL_MS = 1000 / 144; // ~6.94ms for 144fps
314 struct timespec current_time;
315 (void)clock_gettime(CLOCK_MONOTONIC, &current_time);
316 long elapsed_ms = (current_time.tv_sec - last_capture_time.tv_sec) * 1000 +
317 (current_time.tv_nsec - last_capture_time.tv_nsec) / 1000000;
318 if (elapsed_ms < CAPTURE_INTERVAL_MS) {
319 platform_sleep_usec((CAPTURE_INTERVAL_MS - elapsed_ms) * 1000);
320 continue;
321 }
322
323 image_t *image = webcam_read();
324
325 if (!image) {
326 log_info("No frame available from webcam yet (webcam_read returned NULL)");
327 platform_sleep_usec(10000); // 10ms delay before retry
328 continue;
329 }
330
331 // Track frame for FPS reporting
332 (void)clock_gettime(CLOCK_MONOTONIC, &current_time);
333 fps_frame(&fps_tracker, &current_time, "webcam frame captured");
334
335 // Process frame for network transmission
336 // process_frame_for_transmission() always returns a new image that we own
337 // We must free the original image after processing
338 image_t *processed_image = process_frame_for_transmission(image, MAX_FRAME_WIDTH, MAX_FRAME_HEIGHT);
339 if (!processed_image) {
340 SET_ERRNO(ERROR_INVALID_STATE, "Failed to process frame for transmission");
341 // Free the original image from webcam_read() - it's our responsibility
342 image_destroy(image);
343 continue;
344 }
345
346 // Free the original image - we've created a copy/resize in process_frame_for_transmission
347 // The processed_image is a new allocation we own
348 image_destroy(image);
349
350 // processed_image is always a new image (copy or resized) - we own it and must destroy it
351 // Serialize image data for network transmission
352
353 // Check connection before sending
355 log_warn("Connection lost before sending, stopping video transmission");
356 image_destroy(processed_image);
357 break;
358 }
359
360 // Send frame packet to server using proper packet format
361 // This handles the full image_frame_packet_t structure with all required fields
363 asciichat_error_t send_result = acip_send_image_frame(transport, (const void *)processed_image->pixels,
364 (uint32_t)processed_image->w, (uint32_t)processed_image->h,
365 1); // pixel_format = 1 (RGB24)
366
367 if (send_result != ASCIICHAT_OK) {
368 // Signal connection loss for reconnection
369 log_error("Failed to send image frame: %d", send_result);
371 image_destroy(processed_image);
372 break;
373 }
374
375 // FPS tracking - frame successfully captured and sent
376 capture_frame_count++;
377
378 // Calculate time since last frame
379 uint64_t frame_interval_us =
380 ((uint64_t)current_time.tv_sec * 1000000 + (uint64_t)current_time.tv_nsec / 1000) -
381 ((uint64_t)last_capture_frame_time.tv_sec * 1000000 + (uint64_t)last_capture_frame_time.tv_nsec / 1000);
382 last_capture_frame_time = current_time;
383
384 // Expected frame interval in microseconds (6944us for 144fps)
385 uint64_t expected_interval_us = 1000000 / expected_capture_fps;
386 uint64_t lag_threshold_us = expected_interval_us + (expected_interval_us / 2); // 50% over expected
387
388 // Log warning if frame took too long to capture
389 if (capture_frame_count > 1 && frame_interval_us > lag_threshold_us) {
391 "CLIENT CAPTURE LAG: Frame captured %.1fms late (expected %.1fms, got %.1fms, actual fps: %.1f)",
392 (double)(frame_interval_us - expected_interval_us) / 1000.0, (double)expected_interval_us / 1000.0,
393 (double)frame_interval_us / 1000.0, 1000000.0 / (double)frame_interval_us);
394 }
395
396 // Update capture timing
397 last_capture_time = current_time;
398
399 // Clean up resources
400 image_destroy(processed_image);
401 processed_image = NULL;
402 }
403
404#ifdef DEBUG_THREADS
405 log_info("Webcam capture thread stopped");
406#endif
407
408 atomic_store(&g_capture_thread_exited, true);
409
410 // Clean up thread-local error context before exit
412
413 return NULL;
414}
415/* ============================================================================
416 * Public Interface Functions
417 * ============================================================================ */
429 // Initialize webcam capture
430 int webcam_index = GET_OPTION(webcam_index);
431 int result = webcam_init(webcam_index);
432 if (result != 0) {
433 SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam (error code: %d)", result);
434 // Preserve specific error code (e.g., WEBCAM vs WEBCAM_IN_USE)
435 return result;
436 }
437 return 0;
438}
450 if (THREAD_IS_CREATED(g_capture_thread_created)) {
451 log_warn("Capture thread already created");
452 return 0;
453 }
454
455 // Start webcam capture thread
456 atomic_store(&g_capture_thread_exited, false);
457 if (thread_pool_spawn(g_client_worker_pool, webcam_capture_thread_func, NULL, 2, "webcam_capture") != ASCIICHAT_OK) {
458 SET_ERRNO(ERROR_THREAD, "Webcam capture thread creation failed");
459 LOG_ERRNO_IF_SET("Webcam capture thread creation failed");
460 return -1;
461 }
462
463 g_capture_thread_created = true;
464 log_info("Webcam capture thread created successfully");
465
466 // Notify server we're starting to send video
468 LOG_ERRNO_IF_SET("Failed to send stream start packet");
469 }
470
471 return 0;
472}
482 if (!THREAD_IS_CREATED(g_capture_thread_created)) {
483 return;
484 }
485
486 // Flush webcam to interrupt any blocking ReadSample operations
487 // This allows the capture thread to notice should_exit() and exit cleanly
488 webcam_flush();
489
490 // Wait for thread to exit gracefully
491 int wait_count = 0;
492 while (wait_count < 20 && !atomic_load(&g_capture_thread_exited)) {
493 platform_sleep_usec(100000); // 100ms
494 // Keep flushing in case the thread went back into a blocking read
495 if (wait_count % 5 == 0) {
496 webcam_flush();
497 }
498 wait_count++;
499 }
500
501 if (!atomic_load(&g_capture_thread_exited)) {
502 log_warn("Capture thread not responding after 2 seconds - will be joined by thread pool");
503 }
504
505 // Thread will be joined by thread_pool_stop_all() in protocol_stop_connection()
506 g_capture_thread_created = false;
507}
516 return atomic_load(&g_capture_thread_exited);
517}
๐Ÿ”Œ Cross-platform abstraction layer umbrella header for ascii-chat
โš ๏ธโ€ผ๏ธ Error and/or exit() when things go bad.
#define LOG_ERRNO_IF_SET(message)
Check if any error occurred and log it if so.
#define MAX_FRAME_WIDTH
#define MAX_FRAME_HEIGHT
ascii-chat Client Media Capture Management Interface
thread_pool_t * g_client_worker_pool
Global client worker thread pool.
bool should_exit()
Check if client should exit.
๐Ÿ”„ Network byte order conversion helpers
๐Ÿ“Š String Formatting Utilities
โฑ๏ธ FPS tracking utility for monitoring frame throughput across all threads
int capture_start_thread()
Start capture thread.
Definition capture.c:449
int capture_init()
Initialize capture subsystem.
Definition capture.c:428
void capture_cleanup()
Cleanup capture subsystem.
Definition capture.c:526
void capture_stop_thread()
Stop capture thread.
Definition capture.c:481
bool capture_thread_exited()
Check if capture thread has exited.
Definition capture.c:515
bool server_connection_is_active()
Check if server connection is currently active.
acip_transport_t * server_connection_get_transport(void)
Get ACIP transport instance.
int threaded_send_stream_start_packet(uint32_t stream_type)
Thread-safe stream start packet transmission.
bool server_connection_is_lost()
Check if connection loss has been detected.
void server_connection_lost()
Signal that connection has been lost.
unsigned int uint32_t
Definition common.h:58
void fps_frame(fps_t *tracker, const struct timespec *current_time, const char *context)
Track a frame and detect lag conditions.
Definition fps.c:57
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:36
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
void asciichat_errno_cleanup(void)
Cleanup error system resources.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ERROR_INVALID_STATE
@ ERROR_MEMORY
Definition error_codes.h:53
@ ASCIICHAT_OK
Definition error_codes.h:48
@ ERROR_WEBCAM
Definition error_codes.h:61
@ ERROR_THREAD
Definition error_codes.h:95
#define LOG_RATE_FAST
Log rate limit: 1 second (1,000,000 microseconds)
Definition log_rates.h:26
#define log_warn(...)
Log a WARN message.
#define log_error(...)
Log an ERROR message.
#define log_info(...)
Log an INFO message.
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
#define STREAM_TYPE_VIDEO
Video stream.
Definition packet.h:829
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
Definition options.h:644
void platform_sleep_usec(unsigned int usec)
High-precision sleep function with microsecond precision.
pthread_t asciichat_thread_t
Thread handle type (POSIX: pthread_t)
void image_resize(const image_t *s, image_t *d)
Resize image using nearest-neighbor interpolation.
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
Definition video/image.c:85
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
Definition video/image.c:36
asciichat_error_t webcam_init(unsigned short int webcam_index)
Initialize global webcam interface.
Definition webcam.c:18
image_t * webcam_read(void)
Capture a frame from global webcam.
Definition webcam.c:57
void webcam_flush(void)
Flush/interrupt any pending webcam read operations.
Definition webcam.c:219
void webcam_cleanup(void)
Clean up global webcam interface.
Definition webcam.c:204
asciichat_error_t acip_send_image_frame(acip_transport_t *transport, const void *pixel_data, uint32_t width, uint32_t height, uint32_t pixel_format)
Send image frame to server (client โ†’ server)
ACIP client-side protocol library.
โš™๏ธ Command-line options parsing and configuration management for ascii-chat
ACIP shared/bidirectional packet sending functions.
ascii-chat Server Mode Entry Point Header
ascii-chat Client Server Connection Management Interface
RGB pixel structure.
Definition video/image.h:80
Transport instance structure.
Definition transport.h:169
FPS tracking state.
Definition fps.h:51
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)
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.
Definition thread_pool.c:65
๐Ÿงต Generic thread pool abstraction for managing worker threads
โฑ๏ธ High-precision timing utilities using sokol_time.h and uthash
๐Ÿ–ผ๏ธ Safe overflow-checked buffer size calculations for images and video frames
๐Ÿงต Thread lifecycle management helpers
#define THREAD_IS_CREATED(created_flag)
Definition util/thread.h:62
Image Data Structures and Operations.