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>
100#include <ascii-chat/util/fps.h>
101#include <ascii-chat/util/time.h>
102#include <ascii-chat/util/thread.h>
103#include <ascii-chat/network/acip/send.h>
104#include <ascii-chat/network/acip/client.h>
105#include <stdatomic.h>
108#include <ascii-chat/platform/abstraction.h>
109#include <ascii-chat/thread_pool.h>
124static session_capture_ctx_t *g_capture_capture_ctx = NULL;
138static bool g_capture_thread_created =
false;
148static atomic_bool g_capture_thread_exited =
false;
154#define CAPTURE_TARGET_FPS 144
188static void *webcam_capture_thread_func(
void *arg) {
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;
197 if (!fps_tracker_initialized) {
199 fps_tracker_initialized =
true;
205 log_debug_every(LOG_RATE_NORMAL,
"Capture thread: waiting for connection to become active");
223 log_debug_every(LOG_RATE_SLOW,
"Using cached frame while paused");
226 log_debug(
"Media source reached end of file");
231 log_debug_every(LOG_RATE_SLOW,
"No frame available from media source yet (returned NULL)");
244 if (!processed_image) {
245 SET_ERRNO(ERROR_INVALID_STATE,
"Failed to process frame for transmission");
252 log_warn(
"Connection lost before sending, stopping video transmission");
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,
265 if (send_result != ASCIICHAT_OK) {
266 log_error(
"Failed to send image frame: %d", send_result);
271 log_debug_every(LOG_RATE_SLOW,
"Capture thread: IMAGE_FRAME sent successfully");
278 last_frame =
image_new(processed_image->w, processed_image->h);
280 memcpy(last_frame->pixels, processed_image->pixels,
281 (
size_t)processed_image->w * (
size_t)processed_image->h *
sizeof(rgb_pixel_t));
285 capture_frame_count++;
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;
295 uint64_t lag_threshold_ns = expected_interval_ns + (expected_interval_ns / 2);
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);
310 processed_image = NULL;
317 log_debug(
"Webcam capture thread stopped");
326 atomic_store(&g_capture_thread_exited,
true);
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);
353 if (media_url && media_url[0] !=
'\0') {
356 config.type = MEDIA_SOURCE_FILE;
357 config.path = media_url;
359 log_debug(
"Using network URL: %s (webcam disabled)", media_url);
360 }
else if (media_file && media_file[0] !=
'\0') {
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)) {
368 config.type = MEDIA_SOURCE_TEST;
370 log_debug(
"Using test pattern mode");
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));
380 config.resize_for_network =
true;
383 config.enable_audio =
true;
384 config.audio_fallback_to_mic =
true;
388 config.initial_seek_timestamp = GET_OPTION(media_seek_timestamp);
392 if (!g_capture_capture_ctx) {
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;
400 SET_ERRNO(ERROR_MEDIA_INIT,
"Failed to initialize capture source");
417 if (THREAD_IS_CREATED(g_capture_thread_created)) {
418 log_warn(
"Capture thread already created");
423 atomic_store(&g_capture_thread_exited,
false);
425 SET_ERRNO(ERROR_THREAD,
"Webcam capture thread creation failed");
426 LOG_ERRNO_IF_SET(
"Webcam capture thread creation failed");
430 g_capture_thread_created =
true;
431 log_debug(
"Webcam capture thread created successfully");
435 LOG_ERRNO_IF_SET(
"Failed to send stream start packet");
449 if (!THREAD_IS_CREATED(g_capture_thread_created)) {
455 while (wait_count < 20 && !atomic_load(&g_capture_thread_exited)) {
460 if (!atomic_load(&g_capture_thread_exited)) {
461 log_warn(
"Capture thread not responding after 2 seconds - will be joined by thread pool");
465 g_capture_thread_created =
false;
475 return atomic_load(&g_capture_thread_exited);
489 if (g_capture_capture_ctx) {
491 g_capture_capture_ctx = NULL;
void asciichat_errno_destroy(void)
ascii-chat Client Media Capture Management Interface
thread_pool_t * g_client_worker_pool
Global client worker thread pool.
void fps_frame_ns(fps_t *tracker, uint64_t current_time_ns, const char *context)
void fps_init(fps_t *tracker, int expected_fps, const char *name)
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)
ascii-chat Server Mode Entry Point Header
bool media_source_is_paused(media_source_t *source)
media_source_type_t media_source_get_type(media_source_t *source)
ascii-chat Client Audio Processing Management Interface
#define CAPTURE_TARGET_FPS
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
asciichat_error_t thread_pool_spawn(thread_pool_t *pool, void *(*thread_func)(void *), void *thread_arg, int stop_id, const char *thread_name)
uint64_t time_get_ns(void)
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
void image_destroy(image_t *p)
image_t * image_new(size_t width, size_t height)