13#include <ascii-chat/session/capture.h>
14#include <ascii-chat/common.h>
15#include <ascii-chat/log/logging.h>
16#include <ascii-chat/media/source.h>
17#include <ascii-chat/options/options.h>
18#include <ascii-chat/video/image.h>
19#include <ascii-chat/util/time.h>
20#include <ascii-chat/util/fps.h>
21#include <ascii-chat/audio/audio.h>
22#include <ascii-chat/asciichat_errno.h>
33#define SESSION_MAX_FRAME_WIDTH 480
36#define SESSION_MAX_FRAME_HEIGHT 270
117static void calculate_optimal_dimensions(ssize_t original_width, ssize_t original_height, ssize_t max_width,
118 ssize_t max_height, ssize_t *result_width, ssize_t *result_height) {
120 float img_aspect = (float)original_width / (
float)original_height;
123 if (original_width <= max_width && original_height <= max_height) {
125 *result_width = original_width;
126 *result_height = original_height;
131 if ((
float)max_width / (float)max_height > img_aspect) {
133 *result_height = max_height;
134 *result_width = (ssize_t)(max_height * img_aspect);
137 *result_width = max_width;
138 *result_height = (ssize_t)(max_width / img_aspect);
149 return SET_ERRNO(ERROR_INVALID_PARAM,
"Mirror capture requires explicit config"), NULL;
158 session_capture_ctx_t *ctx = SAFE_CALLOC(1,
sizeof(session_capture_ctx_t), session_capture_ctx_t *);
164 ctx->target_fps = target_fps > 0 ? target_fps : 60;
165 ctx->resize_for_network =
false;
168 ctx->audio_enabled =
false;
170 ctx->source_owned =
false;
173 uint64_t baseline_sleep_ns = NS_PER_SEC_INT / ctx->target_fps;
174 adaptive_sleep_config_t sleep_config = {.baseline_sleep_ns = baseline_sleep_ns,
175 .min_speed_multiplier = 0.5,
176 .max_speed_multiplier = 2.0,
178 .slowdown_rate = 0.1};
182 char *tracker_name = SAFE_MALLOC(32,
char *);
188 fps_init(&ctx->fps_tracker, 60, tracker_name);
191 ctx->initialized =
true;
193 log_debug(
"Created network capture context (no local media source)");
199 session_capture_config_t auto_config = {0};
203 const char *media_file = GET_OPTION(media_file);
204 bool media_from_stdin = GET_OPTION(media_from_stdin);
206 if (media_file[0] !=
'\0') {
208 auto_config.type = media_from_stdin ? MEDIA_SOURCE_STDIN : MEDIA_SOURCE_FILE;
209 auto_config.path = media_file;
210 auto_config.loop = GET_OPTION(media_loop) && !media_from_stdin;
211 }
else if (GET_OPTION(test_pattern)) {
213 auto_config.type = MEDIA_SOURCE_TEST;
214 auto_config.path = NULL;
217 static char webcam_index_str[32];
218 safe_snprintf(webcam_index_str,
sizeof(webcam_index_str),
"%u", GET_OPTION(webcam_index));
219 auto_config.type = MEDIA_SOURCE_WEBCAM;
220 auto_config.path = webcam_index_str;
224 auto_config.target_fps = 60;
225 auto_config.resize_for_network =
false;
226 config = &auto_config;
230 if (config->should_exit_callback && config->should_exit_callback(config->callback_data)) {
235 session_capture_ctx_t *ctx = SAFE_CALLOC(1,
sizeof(session_capture_ctx_t), session_capture_ctx_t *);
238 ctx->target_fps = config->target_fps > 0 ? config->target_fps : 60;
239 ctx->resize_for_network = config->resize_for_network;
242 ctx->audio_enabled = config->enable_audio;
243 ctx->audio_fallback_enabled = config->audio_fallback_to_mic;
244 ctx->mic_audio_ctx = config->mic_audio_ctx;
245 ctx->using_file_audio =
false;
246 ctx->file_has_audio =
false;
250 if (config->media_source) {
251 ctx->source = config->media_source;
252 ctx->source_owned =
false;
253 log_debug(
"Using pre-created media source (avoids redundant YouTube extraction)");
256 ctx->source_owned =
true;
261 asciichat_error_t existing_error = GET_ERRNO();
262 if (existing_error == ASCIICHAT_OK) {
263 SET_ERRNO(ERROR_MEDIA_INIT,
"Failed to create media source");
270 if (ctx->audio_enabled && ctx->source) {
272 if (ctx->file_has_audio) {
273 ctx->using_file_audio =
true;
274 log_info(
"Audio capture enabled: using file audio");
275 }
else if (ctx->audio_fallback_enabled && ctx->mic_audio_ctx) {
276 ctx->using_file_audio =
false;
277 log_info(
"Audio capture enabled: file has no audio, using microphone fallback");
279 ctx->audio_enabled =
false;
280 log_debug(
"Audio capture disabled: no file audio and no fallback configured");
285 if (config->loop && config->type == MEDIA_SOURCE_FILE) {
292 if (config->type == MEDIA_SOURCE_FILE && GET_OPTION(pause)) {
293 ctx->should_pause_after_first_frame =
true;
294 log_debug(
"Will pause after first frame (--pause flag)");
298 if (config->initial_seek_timestamp > 0.0) {
299 log_debug(
"Seeking to %.2f seconds", config->initial_seek_timestamp);
300 asciichat_error_t seek_err =
media_source_seek(ctx->source, config->initial_seek_timestamp);
301 if (seek_err != ASCIICHAT_OK) {
302 log_warn(
"Failed to seek to %.2f seconds: %d", config->initial_seek_timestamp, seek_err);
305 log_debug(
"Successfully seeked to %.2f seconds", config->initial_seek_timestamp);
310 ctx->frame_count = 0;
317 float snapshot_delay = GET_OPTION(snapshot_delay);
318 if (GET_OPTION(snapshot_mode) && snapshot_delay == 0.0f) {
322 log_debug(
"Waiting for prefetch thread after seek (snapshot_delay=0, HTTP streams need ~1 second)");
330 uint64_t baseline_sleep_ns = NS_PER_SEC_INT / ctx->target_fps;
331 adaptive_sleep_config_t sleep_config = {
332 .baseline_sleep_ns = baseline_sleep_ns,
333 .min_speed_multiplier = 0.5,
334 .max_speed_multiplier = 2.0,
342 char *tracker_name = SAFE_MALLOC(32,
char *);
348 safe_snprintf(tracker_name, 32,
"CAPTURE_%u", ctx->target_fps);
349 fps_init(&ctx->fps_tracker, (
int)ctx->target_fps, tracker_name);
354 ctx->initialized =
true;
364 if (ctx->source && ctx->source_owned) {
370 if (ctx->fps_tracker.tracker_name) {
371 char *temp = (
char *)ctx->fps_tracker.tracker_name;
375 ctx->initialized =
false;
384 if (!ctx || !ctx->initialized || !ctx->source) {
388 static uint64_t last_frame_time_ns = 0;
397 fps_frame_ns(&ctx->fps_tracker, frame_available_time_ns,
"frame captured");
401 if (last_frame_time_ns > 0) {
402 uint64_t time_since_last_frame_ns =
time_elapsed_ns(last_frame_time_ns, frame_request_time_ns);
403 uint64_t time_to_get_frame_ns =
time_elapsed_ns(frame_request_time_ns, frame_available_time_ns);
404 double since_last_ms = (double)time_since_last_frame_ns / NS_PER_MS;
405 double to_get_ms = (double)time_to_get_frame_ns / NS_PER_MS;
407 if (ctx->frame_count % 30 == 0) {
408 log_dev_every(3 * US_PER_SEC_INT,
"FRAME_TIMING[%lu]: since_last=%.1f ms, to_get=%.1f ms", ctx->frame_count,
409 since_last_ms, to_get_ms);
412 last_frame_time_ns = frame_available_time_ns;
415 if (ctx->should_pause_after_first_frame && !ctx->paused_after_first_frame) {
417 ctx->paused_after_first_frame =
true;
418 log_info(
"Paused (--pause flag)");
426 if (!ctx || !frame) {
427 SET_ERRNO(ERROR_INVALID_PARAM,
"session_capture_process_for_transmission: NULL parameter");
432 if (!ctx->resize_for_network) {
433 image_t *copy =
image_new(frame->w, frame->h);
435 SET_ERRNO(ERROR_MEMORY,
"Failed to allocate image copy");
438 memcpy(copy->pixels, frame->pixels, (
size_t)frame->w * (
size_t)frame->h *
sizeof(rgb_pixel_t));
443 ssize_t resized_width, resized_height;
448 if (frame->w == resized_width && frame->h == resized_height) {
450 image_t *copy =
image_new(frame->w, frame->h);
452 SET_ERRNO(ERROR_MEMORY,
"Failed to allocate image copy");
455 memcpy(copy->pixels, frame->pixels, (
size_t)frame->w * (
size_t)frame->h *
sizeof(rgb_pixel_t));
460 image_t *resized =
image_new(resized_width, resized_height);
462 SET_ERRNO(ERROR_MEMORY,
"Failed to allocate resized image buffer");
473 if (!ctx || !ctx->initialized) {
482 if (!ctx || !ctx->initialized || !ctx->source) {
490 return ctx != NULL && ctx->initialized && ctx->source != NULL;
494 if (!ctx || !ctx->initialized || ctx->frame_count == 0) {
500 double elapsed_sec = time_ns_to_s(elapsed_ns);
502 if (elapsed_sec <= 0.0) {
506 return (
double)ctx->frame_count / elapsed_sec;
513 return ctx->target_fps;
517 if (!ctx || !ctx->initialized) {
520 return ctx->audio_enabled;
524 if (!ctx || !ctx->initialized || !buffer || num_samples == 0) {
528 if (!ctx->audio_enabled) {
533 if (ctx->using_file_audio && ctx->source) {
538 if (ctx->mic_audio_ctx) {
541 audio_context_t *audio_ctx = (audio_context_t *)ctx->mic_audio_ctx;
542 if (audio_ctx && audio_ctx->capture_buffer) {
551 if (!ctx || !ctx->initialized) {
554 return ctx->using_file_audio;
558 if (!ctx || !ctx->initialized || !ctx->source) {
561 return (
void *)ctx->source;
565 if (!ctx || !ctx->initialized) {
568 return ctx->audio_ctx;
573 ctx->audio_ctx = audio_ctx;
582 if (!ctx || !ctx->initialized || !ctx->source) {
583 return ERROR_INVALID_PARAM;
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)
size_t audio_ring_buffer_read(audio_ring_buffer_t *rb, float *data, size_t samples)
void session_capture_set_audio_context(session_capture_ctx_t *ctx, void *audio_ctx)
bool session_capture_has_audio(session_capture_ctx_t *ctx)
image_t * session_capture_process_for_transmission(session_capture_ctx_t *ctx, image_t *frame)
session_capture_ctx_t * session_mirror_capture_create(const session_capture_config_t *config)
bool session_capture_at_end(session_capture_ctx_t *ctx)
double session_capture_get_current_fps(session_capture_ctx_t *ctx)
void * session_capture_get_audio_context(session_capture_ctx_t *ctx)
#define SESSION_MAX_FRAME_WIDTH
Maximum frame width for network transmission (bandwidth optimization)
size_t session_capture_read_audio(session_capture_ctx_t *ctx, float *buffer, size_t num_samples)
image_t * session_capture_read_frame(session_capture_ctx_t *ctx)
bool session_capture_is_valid(session_capture_ctx_t *ctx)
bool session_capture_using_file_audio(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)
session_capture_ctx_t * session_network_capture_create(uint32_t target_fps)
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)
#define SESSION_MAX_FRAME_HEIGHT
Maximum frame height for network transmission (bandwidth optimization)
asciichat_error_t session_capture_sync_audio_to_video(session_capture_ctx_t *ctx)
bool media_source_at_end(media_source_t *source)
size_t media_source_read_audio(media_source_t *source, float *buffer, size_t num_samples)
bool media_source_has_audio(media_source_t *source)
void media_source_destroy(media_source_t *source)
void media_source_pause(media_source_t *source)
image_t * media_source_read_video(media_source_t *source)
asciichat_error_t media_source_seek(media_source_t *source, double timestamp_sec)
media_source_t * media_source_create(media_source_type_t type, const char *path)
asciichat_error_t media_source_sync_audio_to_video(media_source_t *source)
void media_source_set_loop(media_source_t *source, bool loop)
Internal session capture context structure.
fps_t fps_tracker
FPS tracker for monitoring capture rate.
void * mic_audio_ctx
Microphone audio context for fallback (borrowed, not owned)
bool file_has_audio
File has audio stream available.
adaptive_sleep_state_t sleep_state
Adaptive sleep state for frame rate limiting.
bool using_file_audio
Using file audio (true) or microphone fallback (false)
bool paused_after_first_frame
Whether we've already paused after the first frame.
bool audio_enabled
Audio is enabled for capture.
uint32_t target_fps
Target frames per second.
bool source_owned
Whether we own the media source (true) or it was provided externally (false)
uint64_t start_time_ns
Start time for FPS calculation (nanoseconds)
bool initialized
Context has been successfully initialized.
bool should_pause_after_first_frame
Pause media source after first frame is read (–pause flag)
bool resize_for_network
Whether to resize frames for network transmission.
media_source_t * source
Underlying media source (webcam, file, stdin, test)
void * audio_ctx
Main audio context for playback (borrowed, not owned)
bool audio_fallback_enabled
Fall back to microphone if file has no audio.
uint64_t frame_count
Frame count for FPS calculation.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
uint64_t time_get_ns(void)
void adaptive_sleep_init(adaptive_sleep_state_t *state, const adaptive_sleep_config_t *config)
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
void adaptive_sleep_do(adaptive_sleep_state_t *state, size_t queue_depth, size_t target_depth)
void image_resize(const image_t *s, image_t *d)
image_t * image_new(size_t width, size_t height)