ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
src/client/capture.c
Go to the documentation of this file.
1
88#include "capture.h"
89#include "main.h"
90#include "../main.h" // Global exit API
91#include "server.h"
92#include "audio.h"
93#include <ascii-chat/session/capture.h>
94#include <ascii-chat/video/image.h>
95#include <ascii-chat/media/source.h>
96#include <ascii-chat/common.h>
97#include <ascii-chat/asciichat_errno.h>
98#include <ascii-chat/options/options.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
103#include <ascii-chat/network/acip/send.h>
104#include <ascii-chat/network/acip/client.h>
105#include <stdatomic.h>
106#include <time.h>
107#include <string.h>
108#include <ascii-chat/platform/abstraction.h>
109#include <ascii-chat/thread_pool.h>
110
111/* ============================================================================
112 * Session Capture Context
113 * ============================================================================ */
114
124static session_capture_ctx_t *g_capture_capture_ctx = NULL;
125
126/* ============================================================================
127 * Capture Thread Management
128 * ============================================================================ */
129
138static bool g_capture_thread_created = false;
139
148static atomic_bool g_capture_thread_exited = false;
149
150/* ============================================================================
151 * Frame Processing Constants
152 * ============================================================================ */
154#define CAPTURE_TARGET_FPS 144
155
156/* Frame processing now handled by session library via session_capture_process_for_transmission() */
157/* ============================================================================
158 * Capture Thread Implementation
159 * ============================================================================ */
160
188static void *webcam_capture_thread_func(void *arg) {
189 (void)arg;
190
191 // FPS tracking for webcam capture thread
192 static fps_t fps_tracker = {0};
193 static bool fps_tracker_initialized = false;
194 static uint64_t capture_frame_count = 0;
195 static uint64_t last_capture_frame_time_ns = 0;
196 static image_t *last_frame = NULL; // Cache last frame to render when paused
197 if (!fps_tracker_initialized) {
198 fps_init(&fps_tracker, CAPTURE_TARGET_FPS, "WEBCAM_TX");
199 fps_tracker_initialized = true;
200 }
201
202 while (!should_exit() && !server_connection_is_lost()) {
203 // Check connection status
205 log_debug_every(LOG_RATE_NORMAL, "Capture thread: waiting for connection to become active");
206 platform_sleep_us(100 * US_PER_MS_INT); // Wait for connection
207 continue;
208 }
209
210 // Frame rate limiting using session capture adaptive sleep
211 session_capture_sleep_for_fps(g_capture_capture_ctx);
212
213 // Read frame using session capture library
214 image_t *image = session_capture_read_frame(g_capture_capture_ctx);
215
216 // Check if media is paused and we have a last frame - render last frame to keep keyboard polling active
217 if (!image) {
218 media_source_t *source = (media_source_t *)session_capture_get_media_source(g_capture_capture_ctx);
219 if (source && media_source_get_type(source) == MEDIA_SOURCE_FILE && media_source_is_paused(source) &&
220 last_frame) {
221 // Use last frame when paused (keeps display thread rendering and keyboard polling active)
222 image = last_frame;
223 log_debug_every(LOG_RATE_SLOW, "Using cached frame while paused");
224 } else if (session_capture_at_end(g_capture_capture_ctx)) {
225 // Check if we've reached end of file for media sources
226 log_debug("Media source reached end of file");
227 image_destroy(last_frame);
228 last_frame = NULL;
229 break; // Exit capture loop - end of media
230 } else {
231 log_debug_every(LOG_RATE_SLOW, "No frame available from media source yet (returned NULL)");
232 platform_sleep_us(10 * US_PER_MS_INT); // 10ms delay before retry
233 continue;
234 }
235 }
236
237 // Track frame for FPS reporting
238 fps_frame_ns(&fps_tracker, time_get_ns(), "webcam frame captured");
239
240 // Process frame for network transmission using session library
241 // session_capture_process_for_transmission() returns a new image that we own
242 // NOTE: The original 'image' is owned by media_source - do NOT free it!
243 image_t *processed_image = session_capture_process_for_transmission(g_capture_capture_ctx, image);
244 if (!processed_image) {
245 SET_ERRNO(ERROR_INVALID_STATE, "Failed to process frame for transmission");
246 // NOTE: Do NOT free 'image' - it's owned by capture context
247 continue;
248 }
249
250 // Check connection before sending
252 log_warn("Connection lost before sending, stopping video transmission");
253 image_destroy(processed_image);
254 break;
255 }
256
257 // Send frame packet to server using proper packet format
258 acip_transport_t *transport = server_connection_get_transport();
259 log_debug_every(LOG_RATE_SLOW, "Capture thread: sending IMAGE_FRAME %ux%u via transport %p", processed_image->w,
260 processed_image->h, (void *)transport);
261 asciichat_error_t send_result = acip_send_image_frame(transport, (const void *)processed_image->pixels,
262 (uint32_t)processed_image->w, (uint32_t)processed_image->h,
263 1); // pixel_format = 1 (RGB24)
264
265 if (send_result != ASCIICHAT_OK) {
266 log_error("Failed to send image frame: %d", send_result);
268 image_destroy(processed_image);
269 break;
270 }
271 log_debug_every(LOG_RATE_SLOW, "Capture thread: IMAGE_FRAME sent successfully");
272
273 // Cache last frame for rendering when paused
274 // Make a copy since the original is owned by media_source
275 if (last_frame) {
276 image_destroy(last_frame);
277 }
278 last_frame = image_new(processed_image->w, processed_image->h);
279 if (last_frame) {
280 memcpy(last_frame->pixels, processed_image->pixels,
281 (size_t)processed_image->w * (size_t)processed_image->h * sizeof(rgb_pixel_t));
282 }
283
284 // FPS tracking - frame successfully captured and sent
285 capture_frame_count++;
286
287 // Calculate time since last frame for lag detection (using nanosecond precision internally)
288 uint64_t frame_capture_time_ns = time_get_ns();
289
290 uint64_t frame_interval_ns = time_elapsed_ns(last_capture_frame_time_ns, frame_capture_time_ns);
291 last_capture_frame_time_ns = frame_capture_time_ns;
292
293 // Expected frame interval in nanoseconds
294 uint64_t expected_interval_ns = NS_PER_SEC_INT / (uint64_t)session_capture_get_target_fps(g_capture_capture_ctx);
295 uint64_t lag_threshold_ns = expected_interval_ns + (expected_interval_ns / 2); // 50% over expected
296
297 // Log warning if frame took too long to capture (display in milliseconds for readability)
298 if (capture_frame_count > 1 && frame_interval_ns > lag_threshold_ns) {
299 double late_ms = (double)(frame_interval_ns - expected_interval_ns) / 1e6;
300 double expected_ms = (double)expected_interval_ns / 1e6;
301 double actual_ms = (double)frame_interval_ns / 1e6;
302 double actual_fps = 1e9 / (double)frame_interval_ns;
303 log_warn_every(LOG_RATE_FAST,
304 "CLIENT CAPTURE LAG: Frame captured %.1fms late (expected %.1fms, got %.1fms, actual fps: %.1f)",
305 late_ms, expected_ms, actual_ms, actual_fps);
306 }
307
308 // Clean up processed frame
309 image_destroy(processed_image);
310 processed_image = NULL;
311
312 // Yield to reduce CPU usage
313 platform_sleep_us(1 * US_PER_MS_INT); // 1ms
314 }
315
316#ifdef DEBUG_THREADS
317 log_debug("Webcam capture thread stopped");
318#endif
319
320 // Clean up cached frame before thread exit
321 if (last_frame) {
322 image_destroy(last_frame);
323 last_frame = NULL;
324 }
325
326 atomic_store(&g_capture_thread_exited, true);
327
328 // Clean up thread-local error context before exit
330
331 return NULL;
332}
333/* ============================================================================
334 * Public Interface Functions
335 * ============================================================================ */
347 // Build capture configuration from options
348 session_capture_config_t config = {0};
349 const char *media_url = GET_OPTION(media_url);
350 const char *media_file = GET_OPTION(media_file);
351 bool media_from_stdin = GET_OPTION(media_from_stdin);
352
353 if (media_url && media_url[0] != '\0') {
354 // Network URL streaming (takes priority over --file)
355 // Don't open webcam when streaming from URL
356 config.type = MEDIA_SOURCE_FILE;
357 config.path = media_url;
358 config.loop = false; // Network URLs cannot be looped
359 log_debug("Using network URL: %s (webcam disabled)", media_url);
360 } else if (media_file && media_file[0] != '\0') {
361 // File or stdin streaming - don't open webcam
362 config.type = media_from_stdin ? MEDIA_SOURCE_STDIN : MEDIA_SOURCE_FILE;
363 config.path = media_file;
364 config.loop = GET_OPTION(media_loop) && !media_from_stdin;
365 log_debug("Using media %s: %s (webcam disabled)", media_from_stdin ? "stdin" : "file", media_file);
366 } else if (GET_OPTION(test_pattern)) {
367 // Test pattern mode - don't open real webcam
368 config.type = MEDIA_SOURCE_TEST;
369 config.path = NULL;
370 log_debug("Using test pattern mode");
371 } else {
372 // Webcam mode (default)
373 static char webcam_index_str[32];
374 safe_snprintf(webcam_index_str, sizeof(webcam_index_str), "%u", GET_OPTION(webcam_index));
375 config.type = MEDIA_SOURCE_WEBCAM;
376 config.path = webcam_index_str;
377 log_debug("Using webcam device %u", GET_OPTION(webcam_index));
378 }
379 config.target_fps = CAPTURE_TARGET_FPS;
380 config.resize_for_network = true; // Client always resizes for network transmission
381
382 // Configure audio capture with fallback to microphone
383 config.enable_audio = true;
384 config.audio_fallback_to_mic = true;
385 config.mic_audio_ctx = audio_get_context();
386
387 // Add seek timestamp if specified
388 config.initial_seek_timestamp = GET_OPTION(media_seek_timestamp);
389
390 // Create capture context using session library
391 g_capture_capture_ctx = session_capture_create(&config);
392 if (!g_capture_capture_ctx) {
393 // Check if there's already an error set (e.g., ERROR_WEBCAM_IN_USE)
394 asciichat_error_t existing_error = GET_ERRNO();
395 log_debug("session_capture_create failed, GET_ERRNO() returned: %d", existing_error);
396 if (existing_error != ASCIICHAT_OK) {
397 log_debug("Returning existing error code %d", existing_error);
398 return existing_error;
399 }
400 SET_ERRNO(ERROR_MEDIA_INIT, "Failed to initialize capture source");
401 return -1;
402 }
403
404 return 0;
405}
417 if (THREAD_IS_CREATED(g_capture_thread_created)) {
418 log_warn("Capture thread already created");
419 return 0;
420 }
421
422 // Start webcam capture thread
423 atomic_store(&g_capture_thread_exited, false);
424 if (thread_pool_spawn(g_client_worker_pool, webcam_capture_thread_func, NULL, 2, "webcam_capture") != ASCIICHAT_OK) {
425 SET_ERRNO(ERROR_THREAD, "Webcam capture thread creation failed");
426 LOG_ERRNO_IF_SET("Webcam capture thread creation failed");
427 return -1;
428 }
429
430 g_capture_thread_created = true;
431 log_debug("Webcam capture thread created successfully");
432
433 // Notify server we're starting to send video
434 if (threaded_send_stream_start_packet(STREAM_TYPE_VIDEO) < 0) {
435 LOG_ERRNO_IF_SET("Failed to send stream start packet");
436 }
437
438 return 0;
439}
449 if (!THREAD_IS_CREATED(g_capture_thread_created)) {
450 return;
451 }
452
453 // Wait for thread to exit gracefully
454 int wait_count = 0;
455 while (wait_count < 20 && !atomic_load(&g_capture_thread_exited)) {
456 platform_sleep_us(100 * US_PER_MS_INT); // 100ms
457 wait_count++;
458 }
459
460 if (!atomic_load(&g_capture_thread_exited)) {
461 log_warn("Capture thread not responding after 2 seconds - will be joined by thread pool");
462 }
463
464 // Thread will be joined by thread_pool_stop_all() in protocol_stop_connection()
465 g_capture_thread_created = false;
466}
475 return atomic_load(&g_capture_thread_exited);
476}
487
488 // Destroy capture context
489 if (g_capture_capture_ctx) {
490 session_capture_destroy(g_capture_capture_ctx);
491 g_capture_capture_ctx = NULL;
492 }
493}
void asciichat_errno_destroy(void)
ascii-chat Client Media Capture Management Interface
thread_pool_t * g_client_worker_pool
Global client worker thread pool.
bool should_exit(void)
Definition main.c:90
void fps_frame_ns(fps_t *tracker, uint64_t current_time_ns, const char *context)
Definition fps.c:52
void fps_init(fps_t *tracker, int expected_fps, const char *name)
Definition fps.c:32
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.
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_stream_start_packet(uint32_t stream_type)
Thread-safe stream start packet transmission.
void server_connection_lost()
Signal that connection has been lost.
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)
image_t * session_capture_process_for_transmission(session_capture_ctx_t *ctx, image_t *frame)
bool session_capture_at_end(session_capture_ctx_t *ctx)
image_t * session_capture_read_frame(session_capture_ctx_t *ctx)
uint32_t session_capture_get_target_fps(session_capture_ctx_t *ctx)
session_capture_ctx_t * session_capture_create(const session_capture_config_t *config)
void * session_capture_get_media_source(session_capture_ctx_t *ctx)
void session_capture_sleep_for_fps(session_capture_ctx_t *ctx)
void session_capture_destroy(session_capture_ctx_t *ctx)
void platform_sleep_us(unsigned int us)
ascii-chat Server Mode Entry Point Header
bool media_source_is_paused(media_source_t *source)
Definition source.c:926
media_source_type_t media_source_get_type(media_source_t *source)
Definition source.c:834
ascii-chat Client Audio Processing Management Interface
#define CAPTURE_TARGET_FPS
Media source for video and audio capture.
Definition source.c:31
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
asciichat_error_t thread_pool_spawn(thread_pool_t *pool, void *(*thread_func)(void *), void *thread_arg, int stop_id, const char *thread_name)
Definition thread_pool.c:70
uint64_t time_get_ns(void)
Definition util/time.c:48
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Definition util/time.c:90
void image_destroy(image_t *p)
Definition video/image.c:85
image_t * image_new(size_t width, size_t height)
Definition video/image.c:36