ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
common/session/capture.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define SESSION_CAPTURE_LOAD_PATH_BUFFER_SIZE 8192
4#define SESSION_CAPTURE_LOAD_ERROR_BUFFER_SIZE 512
5#define SESSION_CAPTURE_WEBCAM_INDEX_BUFFER_SIZE 32
6#define SESSION_CAPTURE_ERROR_BUFFER_SIZE 512
7#define SESSION_CAPTURE_PATH_BUFFER_SIZE 8192
8#define SESSION_CAPTURE_FAILURE_BUFFER_SIZE 512
9
10/* Existing buffer budgets in bytes, including NUL for text storage. */
11#define CAPTURE_TRACKER_NAME_BUFFER_SIZE 32
12
13#include <ascii-chat/otel.h>
16#include <libplatform/atomic.h>
28
29#include "session/capture.h"
31#include <ascii-chat/common.h>
33#include <ascii-chat/log/log.h>
39#include <ascii-chat/util/fps.h>
42
43#include <stdio.h>
44#include <string.h>
45#include <time.h>
46#include <libplatform/thread.h>
47#include <libplatform/mutex.h>
54
55static bool playback_cancelled(void *data);
56
57/* ============================================================================
58 * Session Capture Constants
59 * ============================================================================ */
60
62#define SESSION_MAX_FRAME_WIDTH 320
63
65#define SESSION_MAX_FRAME_HEIGHT 180
66
67/* ============================================================================
68 * Session Capture Context Structure
69 * ============================================================================ */
70
90 platform_thread_t loader;
91 atomic_t load_cancel;
92 atomic_t playlist_skip;
100
108 atomic_t toggle_media;
109 atomic_t redraw_media;
117
120
123
126
129
134
137
140
142 _Atomic bool audio_enabled;
143
146
149
152
155
157 atomic_ptr_t audio_ctx;
158
161
164};
165
166/* ============================================================================
167 * Internal Helper Functions
168 * ============================================================================ */
169
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
188 // Check if image needs resizing
189 if (original_width <= max_width && original_height <= max_height) {
190 // Image is already within bounds - use as-is
191 *result_width = original_width;
192 *result_height = original_height;
193 return;
194 }
195
196 // Determine scaling factor based on which dimension is the limiting factor
197 if ((float)max_width / (float)max_height > img_aspect) {
198 // Max box is wider than image aspect - scale by height
199 *result_height = max_height;
200 *result_width = (ssize_t)(max_height * img_aspect);
201 } else {
202 // Max box is taller than image aspect - scale by width
203 *result_width = max_width;
204 *result_height = (ssize_t)(max_width / img_aspect);
205 }
206}
207
208/* ============================================================================
209 * Session Capture Lifecycle Functions
210 * ============================================================================ */
211
212static void capture_sync_entries(session_capture_ctx_t *ctx, bool add) {
213 if (add)
214 NAMED_REGISTER_ATOMIC(&ctx->load_cancel, "capture.load_cancel", NULL);
215 else
217 if (add)
218 NAMED_REGISTER_ATOMIC(&ctx->playlist_skip, "capture.playlist_skip", NULL);
219 else
221 if (add)
222 NAMED_REGISTER_ATOMIC(&ctx->source_epoch, "capture.source_epoch", NULL);
223 else
225 if (add)
226 NAMED_REGISTER_ATOMIC(&ctx->frame_serial, "capture.frame_serial", NULL);
227 else
229 if (add)
230 NAMED_REGISTER_ATOMIC(&ctx->awaiting_presentation, "capture.awaiting_presentation", NULL);
231 else
233 if (add)
234 NAMED_REGISTER_ATOMIC(&ctx->toggle_media, "capture.toggle_media", NULL);
235 else
237 if (add)
238 NAMED_REGISTER_ATOMIC(&ctx->redraw_media, "capture.redraw_media", NULL);
239 else
241 if (add)
242 NAMED_REGISTER_ATOMIC(&ctx->non_media_active, "capture.non_media_active", NULL);
243 else
245}
246
248 // Mirror capture requires a config with media source info
249 if (!config) {
250 return SET_ERRNO(ERROR_INVALID_PARAM, "Mirror capture requires explicit config"), NULL;
251 }
252
253 // Delegate to the main implementation
254 return session_capture_create(config);
255}
256
258 // Network modes don't capture media - create minimal context for keyboard/audio only
260 if (!ctx) {
261 return NULL;
262 }
263
264 // Set FPS (use provided value or default to 60)
265 ctx->target_fps = target_fps > 0 ? target_fps : DEFAULT_MAX_FPS;
266 ctx->resize_for_network = false;
267
268 // Audio and keyboard will be set up separately by callers
269 ctx->audio_enabled = false;
270 ctx->source = NULL;
271
272 // Initialize minimal FPS tracker (won't be used but keep structure consistent)
273 char *tracker_name = SAFE_MALLOC(CAPTURE_TRACKER_NAME_BUFFER_SIZE, char *);
274 if (!tracker_name) {
275 SAFE_FREE(ctx);
276 return NULL;
277 }
278 safe_snprintf(tracker_name, CAPTURE_TRACKER_NAME_BUFFER_SIZE, "NETWORK_CAPTURE");
279 fps_init(&ctx->fps_tracker, 60, tracker_name);
280
281 ctx->start_time_ns = time_get_ns();
282 ctx->initialized = true;
283 capture_sync_entries(ctx, true);
284
285 log_debug("Created network capture context (no local media source)");
286 return ctx;
287}
288
290 ASCII_OTEL_SCOPE(SESSION_CAPTURE_CREATE);
291 // Auto-create config from command-line options if NULL
292 session_capture_config_t auto_config = {0};
293 if (!config) {
294 // Auto-initialization from GET_OPTION() only works when options have been parsed
295 // Verify by checking if we can access options (would SET_ERRNO if not)
296 const char *media_file = GET_OPTION(media_file);
297 bool media_from_stdin = GET_OPTION(media_from_stdin);
298
299 if (media_file[0] != '\0') {
300 // File or stdin streaming
301 auto_config.type = media_from_stdin ? MEDIA_SOURCE_STDIN : MEDIA_SOURCE_FILE;
302 auto_config.path = media_file;
303 auto_config.loop = GET_OPTION(media_loop) && !media_from_stdin;
304 } else if (GET_OPTION(test_pattern)) {
305 // Test pattern mode
306 auto_config.type = MEDIA_SOURCE_TEST;
307 auto_config.path = NULL;
308 } else {
309 // Webcam mode (default)
310 static char webcam_index_str[SESSION_CAPTURE_WEBCAM_INDEX_BUFFER_SIZE];
311 unsigned int idx = GET_OPTION(webcam_index);
312 safe_snprintf(webcam_index_str, sizeof(webcam_index_str), "%u", idx);
313 auto_config.type = MEDIA_SOURCE_WEBCAM;
314 auto_config.path = webcam_index_str;
315 log_debug("Webcam mode: using device index %u", idx);
316 }
317
318 // Default settings suitable for local display
319 auto_config.target_fps = DEFAULT_MAX_FPS;
320 auto_config.resize_for_network = false;
321 config = &auto_config;
322 }
323
324 // Check if we should exit before starting initialization
325 if (config->should_exit_callback && config->should_exit_callback(config->callback_data)) {
326 return NULL;
327 }
328
329 // Allocate context
331
332 if (!ctx)
333 return NULL;
334 if (mutex_init(&ctx->source_mutex, "capture_source") != 0) {
335 SAFE_FREE(ctx);
336 return NULL;
337 }
338 if (mutex_init(&ctx->load_mutex, "capture_load") != 0) {
339 mutex_destroy(&ctx->source_mutex);
340 SAFE_FREE(ctx);
341 return NULL;
342 }
343 ctx->media_locks_ready = true;
344 if (config->type == MEDIA_SOURCE_FILE && config->path) {
345 media_playlist_t configured = GET_OPTION(media_playlist);
346 if (configured.count && strcmp(configured.paths[0], config->path) == 0)
347 ctx->playlist = configured;
348 else if (media_playlist_append(&ctx->playlist, config->path, strstr(config->path, "://") != NULL) != ASCIICHAT_OK)
349 CLEAR_ERRNO();
350 }
351
352 // Store configuration
353 ctx->target_fps = config->target_fps > 0 ? config->target_fps : DEFAULT_MAX_FPS;
356 ctx->callback_data = config->callback_data;
357
358 // Store audio configuration
359 ctx->audio_enabled = config->enable_audio;
361 ctx->mic_audio_ctx = config->mic_audio_ctx;
362 ctx->using_file_audio = false;
363 ctx->file_has_audio = false;
364
365 // Create media source from config type and path
367 ctx->source = media_source_create_interruptible(config->type, config->path, playback_cancelled, ctx);
368
369 if (!ctx->source) {
370 // Preserve existing error if set, otherwise set generic error
372 asciichat_error_t existing_error = errno_peek_since(create_scope, &failure) ? failure.code : ASCIICHAT_OK;
373 if (existing_error == ASCIICHAT_OK) {
374 SET_ERRNO(ERROR_MEDIA_INIT, "Failed to create media source");
375 }
376 if (config->type == MEDIA_SOURCE_FILE && !playback_cancelled(ctx))
377 NOTICE(WARNING, "MEDIA COULD NOT BE OPENED", "Failed to open media: %s", config->path ? config->path : "");
378 if (ctx->media_locks_ready) {
379 mutex_destroy(&ctx->load_mutex);
380 mutex_destroy(&ctx->source_mutex);
381 }
382 SAFE_FREE(ctx);
383 return NULL;
384 }
385
388 if (ctx->media_locks_ready) {
389 mutex_destroy(&ctx->load_mutex);
390 mutex_destroy(&ctx->source_mutex);
391 }
392 SAFE_FREE(ctx);
393 return NULL;
394 }
395
396 if (config->type == MEDIA_SOURCE_WEBCAM && GET_OPTION(fps) == 0 && !GET_OPTION(waveform) && !GET_OPTION(fft)) {
397 double source_fps = media_source_get_video_fps(ctx->source);
398 if (source_fps >= 0.5 && source_fps <= 1000.0) {
399 ctx->target_fps = (uint32_t)(source_fps + 0.5);
400 log_info("Using negotiated webcam capture rate: %.3f FPS", source_fps);
401 } else {
402 log_warn("Webcam capture rate unavailable; using %u FPS", ctx->target_fps);
403 }
404 }
405
406 if (config->type == MEDIA_SOURCE_FILE) {
407 ctx->saved_matrix = GET_OPTION(matrix_rain);
408 ctx->saved_waveform = GET_OPTION(waveform);
409 ctx->saved_fft = GET_OPTION(fft);
410 // Explicit visualization flags also apply to the initial media source.
411 }
412
413 // Set up exit callback on media source for graceful shutdown during I/O
414 // This allows Ctrl+C to abort blocking FFmpeg calls like YouTube HTTP requests
415 media_source_set_exit_callback(ctx->source, playback_cancelled, ctx);
416
417 // Detect if media source has audio
418 if (ctx->audio_enabled && ctx->source) {
420 if (ctx->file_has_audio) {
421 ctx->using_file_audio = true;
422 log_info("Audio capture enabled: using file audio");
423 } else if (ctx->audio_fallback_enabled && ctx->mic_audio_ctx) {
424 ctx->using_file_audio = false;
425 log_info("Audio capture enabled: file has no audio, using microphone fallback");
426 } else {
427 ctx->audio_enabled = false;
428 log_debug("Audio capture disabled: no file audio and no fallback configured");
429 }
430 }
431
432 // Enable loop if requested (only for file sources)
433 if (config->loop && config->type == MEDIA_SOURCE_FILE && ctx->playlist.count <= 1) {
434 media_source_set_loop(ctx->source, true);
435 }
436
437 // Set initial pause state if requested (only for file sources)
438 // Note: We don't pause yet - we'll pause AFTER reading the first frame in the render loop
439 // This is because paused sources return NULL from read_video()
440 if (config->type == MEDIA_SOURCE_FILE && GET_OPTION(pause)) {
442 log_debug("Will pause after first frame (--pause flag)");
443 }
444
445 // Perform initial seek if requested
446 if (config->initial_seek_timestamp > 0.0) {
447 log_debug("Seeking to %.2f seconds", config->initial_seek_timestamp);
449 if (seek_err != ASCIICHAT_OK) {
450 log_warn("Failed to seek to %.2f seconds: %d", config->initial_seek_timestamp, seek_err);
451 // Continue anyway - seeking is best-effort
452 } else {
453 log_debug("Successfully seeked to %.2f seconds", config->initial_seek_timestamp);
454 // Codec buffers are flushed during seek, so next frame read will use correct position
455
456 // Reset timing state after seek to prevent FPS calculation confusion
457 // (frame_count and elapsed_time must match the new playback position)
458 ctx->frame_count = 0;
459 ctx->start_time_ns = time_get_ns();
460 }
461 }
462
463 // Initialize FPS tracker
464 // Note: Must allocate tracker_name on heap since fps_init stores a pointer to it
465 char *tracker_name = SAFE_MALLOC(CAPTURE_TRACKER_NAME_BUFFER_SIZE, char *);
466 if (!tracker_name) {
468 if (ctx->media_locks_ready) {
469 mutex_destroy(&ctx->load_mutex);
470 mutex_destroy(&ctx->source_mutex);
471 }
472 SAFE_FREE(ctx);
473 return NULL;
474 }
475 safe_snprintf(tracker_name, CAPTURE_TRACKER_NAME_BUFFER_SIZE, "CAPTURE_%u", ctx->target_fps);
476 fps_init(&ctx->fps_tracker, (int)ctx->target_fps, tracker_name);
477
478 // Record start time for FPS calculation (nanoseconds)
479 ctx->start_time_ns = time_get_ns();
480
481 ctx->initialized = true;
482 capture_sync_entries(ctx, true);
483 return ctx;
484}
485
487 if (!ctx) {
488 return;
489 }
490
491 if (ctx->loader_created) {
492 atomic_store_bool(&ctx->load_cancel, true);
493 ASCII_OTEL_SCOPE(MEDIA_TRANSITION_COMMIT);
494 if (thread_join(&ctx->loader, NULL) != 0) {
495 log_error("Cannot join media loader; retaining capture resources");
496 return;
497 }
499 }
500 if (ctx->loaded_frame)
502 if (ctx->initial_frame)
504 if (ctx->loaded_source) {
506 ctx->loaded_source = NULL;
507 }
508 if (ctx->media_locks_ready) {
509 mutex_destroy(&ctx->load_mutex);
510 mutex_destroy(&ctx->source_mutex);
511 }
512 if (ctx->webcam_source)
514 if (ctx->last_media_frame)
516 if (ctx->empty_frame)
519 ctx->face_follow = NULL;
520 // Destroy media source
521 if (ctx->source) {
523 ctx->source = NULL;
524 }
525
526 // Free FPS tracker name (allocated in session_capture_create)
527 if (ctx->fps_tracker.tracker_name) {
528 char *temp = (char *)ctx->fps_tracker.tracker_name;
529 SAFE_FREE(temp);
530 }
531
532 ctx->initialized = false;
533 capture_sync_entries(ctx, false);
534 SAFE_FREE(ctx);
535}
536
537/* ============================================================================
538 * Session Capture Operations
539 * ============================================================================ */
540
541static bool load_cancelled(void *data) {
542 session_capture_ctx_t *ctx = data;
543 return playback_cancelled(ctx) || atomic_load_bool(&ctx->load_cancel);
544}
545
546static bool playback_cancelled(void *data) {
547 session_capture_ctx_t *ctx = data;
549}
550
551static void *load_media_worker(void *data) {
552 ASCII_OTEL_SCOPE(MEDIA_TRANSITION_LOAD);
553 session_capture_ctx_t *ctx = data;
556 if (source) {
557 media_source_set_exit_callback(source, load_cancelled, ctx);
558 media_source_set_loop(source, GET_OPTION(media_loop) && ctx->load_playlist.count <= 1);
561 source = NULL;
562 }
563 }
564 image_t *first = NULL;
566 uint64_t deadline = time_get_ns() + 15 * NS_PER_SEC_INT;
567 while (!load_cancelled(ctx) && time_get_ns() < deadline) {
570 if (frame) {
571 first = image_new(frame->w, frame->h);
572 if (first)
573 memcpy(first->pixels, frame->pixels, (size_t)frame->w * frame->h * sizeof(rgb_pixel_t));
574 break;
575 }
576 asciichat_error_context_t read_failure;
577 if (errno_peek_since(read_scope, &read_failure) || media_source_at_end(source))
578 break;
580 }
581 if (!first) {
583 source = NULL;
584 }
585 }
586 ASCII_OTEL_ATTR_STRING("media.transition.outcome", source ? "ready" : load_cancelled(ctx) ? "cancelled" : "failed");
587 if (!source && !load_cancelled(ctx))
588 ascii_otel_error("Media transition could not prepare source");
590 char error[SESSION_CAPTURE_ERROR_BUFFER_SIZE] = "Could not open media";
591 if (!source && errno_peek_since(scope, &failure))
592 snprintf(error, sizeof(error), "%s", failure.context_message);
594 mutex_lock(&ctx->load_mutex);
595 ctx->loaded_source = source;
596 ctx->loaded_frame = first;
597 snprintf(ctx->load_error, sizeof(ctx->load_error), "%s", error);
598 ctx->loader_done = true;
599 mutex_unlock(&ctx->load_mutex);
600 return NULL;
601}
602
604 if (!ctx || !ctx->media_locks_ready)
605 return false;
606 mutex_lock(&ctx->load_mutex);
607 bool busy = ctx->loader_created;
608 if (busy)
609 atomic_store_bool(&ctx->load_cancel, true);
610 mutex_unlock(&ctx->load_mutex);
611 if (busy)
613 return busy;
614}
615
616static asciichat_error_t capture_load_playlist(session_capture_ctx_t *ctx, const media_playlist_t *list, size_t index,
617 bool automatic) {
618 const char *path = list->paths[index];
619 if (!ctx || !ctx->media_locks_ready || !path || strlen(path) >= sizeof(ctx->load_path))
620 return SET_ERRNO(ERROR_INVALID_STATE, "Local capture is unavailable");
621 size_t limit = strstr(path, "://") ? sizeof(((options_t *)0)->media_url) : sizeof(((options_t *)0)->media_file);
622 if (strlen(path) >= limit)
623 return SET_ERRNO(ERROR_INVALID_PARAM, "Media path or URL exceeds the configured source limit");
624 mutex_lock(&ctx->load_mutex);
625 if (ctx->loader_created || ctx->loaded_source) {
626 mutex_unlock(&ctx->load_mutex);
627 return SET_ERRNO(ERROR_INVALID_STATE, "Previous media load is still active; Escape cancels loading");
628 }
629 ctx->load_playlist = *list;
631 ctx->load_is_advance = automatic;
632 snprintf(ctx->load_path, sizeof(ctx->load_path), "%s", path);
633 ctx->loader_done = false;
634 atomic_store_bool(&ctx->awaiting_presentation, false);
635 atomic_store_bool(&ctx->load_cancel, false);
637 if (progress != ASCIICHAT_OK) {
638 mutex_unlock(&ctx->load_mutex);
639 return progress;
640 }
641 if (platform_thread_create(&ctx->loader, "media_open", load_media_worker, ctx) != 0) {
643 mutex_unlock(&ctx->load_mutex);
644 return SET_ERRNO(ERROR_THREAD, "Cannot start media loader");
645 }
646 ctx->loader_created = true;
647 mutex_unlock(&ctx->load_mutex);
648 return ASCIICHAT_OK;
649}
650
652 ASCII_OTEL_SCOPE(SESSION_CAPTURE_OPEN_MEDIA);
653 media_playlist_t list = {0};
654 asciichat_error_t result = media_playlist_append(&list, path, path && strstr(path, "://") != NULL);
655 return result == ASCIICHAT_OK ? capture_load_playlist(ctx, &list, 0, false) : result;
656}
658 ASCII_OTEL_SCOPE(SESSION_CAPTURE_OPEN_PLAYLIST);
659 media_playlist_t list = {0};
660 asciichat_error_t result = media_playlist_parse(text, &list);
661 return result == ASCIICHAT_OK ? capture_load_playlist(ctx, &list, 0, false) : result;
662}
664 ASCII_OTEL_SCOPE(SESSION_CAPTURE_SKIP_MEDIA);
665 if (ctx && (direction == -1 || direction == 1))
666 atomic_store_u64(&ctx->playlist_skip, direction > 0 ? 1 : 2);
667}
668
669/* Only the capture owner commits replacements, between frame reads. */
670static void capture_commit_media(session_capture_ctx_t *ctx) {
671 if (!ctx->media_locks_ready || !ctx->loader_created || !ctx->loader_done)
672 return;
673 mutex_lock(&ctx->load_mutex);
674 if (!ctx->loader_created || !ctx->loader_done) {
675 mutex_unlock(&ctx->load_mutex);
676 return;
677 }
678 // Media replacement must not inherit a discarded frame's sampling decision.
679 ASCII_OTEL_SCOPE_PARENT(MEDIA_TRANSITION_COMMIT, &(const gotel_context_t){0});
680 if (thread_join(&ctx->loader, NULL) != 0) {
681 mutex_unlock(&ctx->load_mutex);
682 log_error("Cannot join media loader; deferring replacement");
683 return;
684 }
686 snprintf(path, sizeof(path), "%s", ctx->load_path);
687 snprintf(failure, sizeof(failure), "%s", ctx->load_error);
688 ctx->loader_created = false;
689 media_source_t *replacement = ctx->loaded_source;
690 ctx->loaded_source = NULL;
691 image_t *first = ctx->loaded_frame;
692 ctx->loaded_frame = NULL;
693 media_playlist_t loaded_playlist = ctx->load_playlist;
694 bool cancelled = atomic_load_bool(&ctx->load_cancel);
695 bool automatic = ctx->load_is_advance;
696 ASCII_OTEL_ATTR_STRING("media.transition.kind", automatic ? "playlist_advance" : "user");
697 ASCII_OTEL_ATTR_STRING("media.transition.outcome", cancelled ? "cancelled" : "failed");
698 mutex_unlock(&ctx->load_mutex);
699 if (cancelled || !replacement) {
700 ascii_otel_ui_usage("media_input_prompt", cancelled ? "load_cancelled" : "load_failed", 0);
701 ctx->advance_failed |= automatic;
703 if (first)
704 image_destroy(first);
705 if (replacement)
706 media_source_destroy(replacement);
707 if (!cancelled)
708 NOTICE(WARNING, "MEDIA COULD NOT BE OPENED", "%s", failure);
709 CLEAR_ERRNO();
710 return;
711 }
712 audio_context_t *audio = atomic_ptr_load(&ctx->audio_ctx);
713 if (audio && audio_stop_duplex(audio) != ASCIICHAT_OK) {
714 ascii_otel_ui_usage("media_input_prompt", "load_failed", 0);
715 ctx->advance_failed |= automatic;
717 if (first)
718 image_destroy(first);
719 media_source_destroy(replacement);
720 LOG_ERRNO_IF_SET("Cannot stop audio for media replacement");
721 CLEAR_ERRNO();
722 return;
723 }
724 bool has_audio = media_source_has_audio(replacement);
725 media_source_set_exit_callback(replacement, playback_cancelled, ctx);
726 if (audio) {
727 media_source_t *old_audio_source = audio->capture_media_source;
728 bool old_playback_only = audio->playback_only;
729 bool use_media = has_audio && GET_OPTION(audio_capture_source) != AUDIO_CAPTURE_SOURCE_MIC;
730 audio->capture_media_source = use_media ? replacement : NULL;
731 audio->playback_only = !audio_should_enable_microphone(GET_OPTION(audio_capture_source), has_audio);
732 media_source_set_audio_context(replacement, audio);
734 if (audio->capture_buffer)
736 if (audio_start_duplex(audio) != ASCIICHAT_OK) {
737 ascii_otel_ui_usage("media_input_prompt", "load_failed", 0);
738 ctx->advance_failed |= automatic;
740 LOG_ERRNO_IF_SET("Cannot start replacement audio; restoring previous source");
741 CLEAR_ERRNO();
742 if (first)
743 image_destroy(first);
744 if (audio_stop_duplex(audio) != ASCIICHAT_OK) {
745 /* A live worker may still borrow replacement; retain it until teardown. */
746 mutex_lock(&ctx->load_mutex);
747 ctx->loaded_source = replacement;
748 mutex_unlock(&ctx->load_mutex);
749 NOTICE(WARNING, "MEDIA AUDIO", "Could not stop replacement audio");
750 CLEAR_ERRNO();
751 return;
752 }
753 audio->capture_media_source = old_audio_source;
754 audio->playback_only = old_playback_only;
755 media_source_destroy(replacement);
756 if (audio_start_duplex(audio) != ASCIICHAT_OK) {
757 LOG_ERRNO_IF_SET("Cannot restore previous audio");
758 CLEAR_ERRNO();
759 }
760 NOTICE(WARNING, "MEDIA AUDIO", "Could not start replacement audio; previous video retained");
761 CLEAR_ERRNO();
762 return;
763 }
764 }
765 media_source_t *previous = ctx->source;
766 bool was_file = media_source_get_type(previous) == MEDIA_SOURCE_FILE;
767 if (!was_file && !ctx->webcam_source && media_source_uses_webcam(previous)) {
768 ctx->webcam_source = previous;
769 ctx->webcam_attempted = true;
770 previous = NULL;
771 }
772 ctx->source = replacement;
773 if (ctx->initial_frame)
775 ctx->initial_frame = first;
776 ctx->initial_frame_pending = first != NULL;
777 ctx->file_has_audio = has_audio;
779 ctx->audio_enabled = audio != NULL;
780 ctx->saved_audio_source = replacement;
781 ctx->non_media_requested = false;
782 atomic_store_bool(&ctx->non_media_active, false);
783 atomic_store_u64(&ctx->toggle_media, 0);
784 if (!was_file) {
785 ctx->saved_matrix = GET_OPTION(matrix_rain);
786 ctx->saved_waveform = GET_OPTION(waveform);
787 ctx->saved_fft = GET_OPTION(fft);
788 }
789 options_set_bool("matrix_rain", false);
790 options_set_bool("waveform", false);
791 options_set_bool("fft", false);
792 ctx->retained_frame_shown = false;
793 if (ctx->last_media_frame)
795 ctx->last_media_frame = NULL;
797 ctx->face_follow = NULL;
798 ctx->face_media_valid = false;
799 ctx->next_frame_deadline_ns = 0;
800 ctx->frame_count = 0;
801 ctx->start_time_ns = time_get_ns();
802 if (GET_OPTION(fps) == 0) {
803 double fps = media_source_get_video_fps(replacement);
804 if (fps >= 0.5 && fps <= UINT32_MAX - 0.5) {
805 ctx->target_fps = (uint32_t)(fps + 0.5);
807 }
808 }
810 ctx->paused_after_first_frame = false;
811 if (audio) {
815 }
816 if (previous)
817 media_source_destroy(previous);
818 bool url = strstr(path, "://") != NULL;
819 options_set_bool("media_input", true);
820 options_set_string("media_url", url ? path : "");
821 options_set_string("media_file", url ? "" : path);
822 options_set_bool("media_from_stdin", false);
823 mutex_lock(&ctx->load_mutex);
824 ctx->playlist = loaded_playlist;
825 ctx->advance_failed = false;
826 atomic_fetch_add_u64(&ctx->source_epoch, 1);
827 atomic_store_bool(&ctx->awaiting_presentation, !ctx->loader_created);
828 mutex_unlock(&ctx->load_mutex);
829 ascii_otel_ui_usage("media_input_prompt", "load_succeeded", 0);
830 ASCII_OTEL_ATTR_STRING("media.transition.outcome", "committed");
831 log_info("Opened media: %s", path);
832 CLEAR_ERRNO();
833}
834
836 if (ctx && ctx->media_locks_ready)
837 mutex_lock(&ctx->source_mutex);
838}
840 if (ctx && ctx->media_locks_ready)
841 mutex_unlock(&ctx->source_mutex);
842}
843
844static image_t *capture_read_frame(session_capture_ctx_t *ctx) {
845 if (ctx->audio_update) {
846 atomic_ptr_store(&ctx->audio_ctx, ctx->audio_update(ctx->source, ctx->audio_update_data));
847 void *audio_ctx = atomic_ptr_load(&ctx->audio_ctx);
848 ctx->audio_enabled = audio_ctx && ((audio_context_t *)audio_ctx)->running;
849 }
850 capture_commit_media(ctx);
851 bool loading = ctx->loader_created;
852 if (!loading) {
853 uint64_t skip = atomic_exchange_u64(&ctx->playlist_skip, 0);
854 bool ended =
856 size_t next;
857 bool adjacent = media_playlist_adjacent(&ctx->playlist, skip == 2 ? -1 : 1, &next);
858 if (!skip && ended && !adjacent && GET_OPTION(media_loop) && ctx->playlist.count > 1) {
859 next = 0;
860 adjacent = true;
861 }
862 if ((skip || (ended && !ctx->advance_failed)) && adjacent) {
863 if (capture_load_playlist(ctx, &ctx->playlist, next, !skip) != ASCIICHAT_OK) {
864 ctx->advance_failed |= !skip;
865 LOG_ERRNO_IF_SET("Cannot advance media playlist");
866 CLEAR_ERRNO();
867 }
868 }
869 }
870 if (!ctx || !ctx->initialized || !ctx->source) {
871 return NULL;
872 }
873
874 if (ctx->initial_frame && !ctx->initial_frame_pending) {
876 ctx->initial_frame = NULL;
877 }
878 static uint64_t last_frame_time_ns = 0;
879 uint64_t frame_request_time_ns = time_get_ns();
880
881 bool is_file = media_source_get_type(ctx->source) == MEDIA_SOURCE_FILE;
882 if (is_file && (atomic_exchange_u64(&ctx->toggle_media, 0) & 1)) {
883 bool leaving = !atomic_load_bool(&ctx->non_media_active);
884 if (leaving) {
887 }
888 audio_context_t *audio = atomic_ptr_load(&ctx->audio_ctx);
889 if (audio && audio_stop_duplex(audio) == ASCIICHAT_OK) {
890 if (leaving) {
894 audio->capture_media_source = NULL;
895 audio->playback_only = !audio_should_enable_microphone(GET_OPTION(audio_capture_source), false);
896 audio->monitor_local_media = false;
897 } else {
901 }
902 if (audio->capture_buffer)
904 if (audio_start_duplex(audio) != ASCIICHAT_OK) {
905 LOG_ERRNO_IF_SET("Failed to switch audio input");
906 CLEAR_ERRNO();
907 }
908 }
909 if (!leaving && !ctx->media_was_paused)
911 atomic_store_bool(&ctx->non_media_active, leaving);
912 ctx->face_media_valid = false;
914 ctx->face_follow = NULL;
915 atomic_store_bool(&ctx->redraw_media, true);
916 ctx->next_frame_deadline_ns = 0;
917 log_info("Media playback %s at %.3fs (%s)", leaving ? "hidden" : "selected", media_source_get_position(ctx->source),
918 ctx->media_was_paused ? "paused" : "playing");
919 }
920 bool alternate = atomic_load_bool(&ctx->non_media_active);
921 if (alternate && !ctx->webcam_attempted) {
922 ctx->webcam_attempted = true;
926 ctx->webcam_source = NULL;
927 }
928 if (!ctx->webcam_source) {
929 LOG_ERRNO_IF_SET("Webcam unavailable; animations remain available");
930 CLEAR_ERRNO();
931 ctx->empty_frame = image_new(320, 180);
932 if (ctx->empty_frame)
933 memset(ctx->empty_frame->pixels, 0, 320 * 180 * sizeof(rgb_pixel_t));
934 }
935 }
936 image_t *frame;
937 if (!alternate && ctx->initial_frame_pending) {
938 frame = ctx->initial_frame;
939 ctx->initial_frame_pending = false;
940 } else {
941 frame = alternate ? (ctx->webcam_source ? media_source_read_video(ctx->webcam_source) : ctx->empty_frame)
943 }
944 if (frame && is_file && !alternate) {
945 double now = (double)time_get_ns() / 1e9;
946 double position = media_source_get_position(ctx->source);
947 if (ctx->face_media_valid && (position < ctx->face_media_position - 0.1 ||
948 position > ctx->face_media_position + (now - ctx->face_media_tick) + 0.75)) {
950 ctx->face_follow = NULL;
951 }
952 ctx->face_media_valid = true;
953 ctx->face_media_position = position;
954 ctx->face_media_tick = now;
955 }
956 bool recording = GET_OPTION(render_file)[0] != '\0';
957 if (is_file && !alternate && (!GET_OPTION(snapshot_mode) || recording)) {
958 if (frame) {
959 ctx->retained_frame_shown = false;
960 if (!ctx->last_media_frame || ctx->last_media_frame->w != frame->w || ctx->last_media_frame->h != frame->h) {
961 if (ctx->last_media_frame)
963 ctx->last_media_frame = image_new(frame->w, frame->h);
964 }
965 if (ctx->last_media_frame)
966 memcpy(ctx->last_media_frame->pixels, frame->pixels, (size_t)frame->w * frame->h * sizeof(rgb_pixel_t));
967 } else if ((!recording && media_source_at_end(ctx->source)) || media_source_is_paused(ctx->source)) {
968 if (!recording && media_source_at_end(ctx->source))
970 bool redraw = atomic_exchange_bool(&ctx->redraw_media, false);
971 // A paused recording still advances session time, including when stopped
972 // before playback resumes. Retained frames do not increment frame_serial.
973 if (!ctx->retained_frame_shown || redraw || recording) {
974 ctx->retained_frame_shown = true;
975 bool debug = false;
976#ifndef NDEBUG
977 debug = GET_OPTION(face_follow_debug);
978#endif
979 return face_follow_image_process(&ctx->face_follow, ctx->last_media_frame, (double)time_get_ns() / 1e9,
980 GET_OPTION(face_follow), debug,
983 }
984 return NULL;
985 }
986 }
987 stats_duration_record(stats_runtime_scope(), STATS_DURATION_CAPTURE, time_get_ns() - frame_request_time_ns);
988 if (frame) {
992 }
993
994 if (frame) {
995 uint64_t frame_available_time_ns = time_get_ns();
996
997 // Track frame for FPS reporting
998 fps_frame_ns(&ctx->fps_tracker, frame_available_time_ns, "frame captured");
999 ctx->frame_count++;
1000 if (ctx->observed_rate_start_ns == 0)
1001 ctx->observed_rate_start_ns = frame_available_time_ns;
1002 ctx->observed_rate_frames++;
1003 if (ctx->source && media_source_uses_webcam(ctx->source) && GET_OPTION(fps) == 0 &&
1004 frame_available_time_ns - ctx->observed_rate_start_ns >= NS_PER_SEC_INT) {
1005 double elapsed = (double)(frame_available_time_ns - ctx->observed_rate_start_ns) / (double)NS_PER_SEC_INT;
1006 unsigned int observed_fps = (unsigned int)(ctx->observed_rate_frames / elapsed + 0.5);
1007 if (observed_fps > 0) {
1008 log_info("Webcam OS delivery: %.3f FPS over %.2f seconds (%llu frames)",
1009 (double)ctx->observed_rate_frames / elapsed, elapsed, (unsigned long long)ctx->observed_rate_frames);
1010 ui_controller_set_effective_fps(observed_fps);
1011 }
1012 ctx->observed_rate_start_ns = frame_available_time_ns;
1013 ctx->observed_rate_frames = 0;
1014 }
1015 atomic_fetch_add_u64(&ctx->frame_serial, 1);
1016
1017 // Log frame-to-frame timing
1018 if (last_frame_time_ns > 0) {
1019 uint64_t time_since_last_frame_ns = time_elapsed_ns(last_frame_time_ns, frame_request_time_ns);
1020 uint64_t time_to_get_frame_ns = time_elapsed_ns(frame_request_time_ns, frame_available_time_ns);
1021 double since_last_ms = (double)time_since_last_frame_ns / NS_PER_MS;
1022 double to_get_ms = (double)time_to_get_frame_ns / NS_PER_MS;
1023
1024 if (ctx->frame_count % 30 == 0) {
1025 log_dev_every(3 * US_PER_SEC_INT, "FRAME_TIMING[%lu]: since_last=%.1f ms, to_get=%.1f ms", ctx->frame_count,
1026 since_last_ms, to_get_ms);
1027 }
1028 }
1029 last_frame_time_ns = frame_available_time_ns;
1030
1031 // Handle --pause flag: pause after first frame is read
1034 ctx->paused_after_first_frame = true;
1035 log_info("Paused (--pause flag)");
1036 }
1037 }
1038
1039 bool debug = false;
1040#ifndef NDEBUG
1041 debug = GET_OPTION(face_follow_debug);
1042#endif
1043 return face_follow_image_process(&ctx->face_follow, frame, (double)time_get_ns() / 1e9, GET_OPTION(face_follow),
1044 debug, ctx->resize_for_network ? SESSION_MAX_FRAME_WIDTH : 960,
1046}
1047
1049 ASCII_OTEL_SCOPE(SESSION_CAPTURE_READ_FRAME);
1050 if (!ctx)
1051 return NULL;
1053 image_t *frame = capture_read_frame(ctx);
1055 return frame;
1056}
1057
1059 image_t **output) {
1060 ASCII_OTEL_SCOPE(SESSION_CAPTURE_PROCESS_FOR_TRANSMISSION_INTO);
1061 if (!ctx || !frame || !output)
1062 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing transmission frame or workspace");
1063 ssize_t width = frame->w, height = frame->h;
1064 if (ctx->resize_for_network)
1065 calculate_optimal_dimensions(frame->w, frame->h, SESSION_MAX_FRAME_WIDTH, SESSION_MAX_FRAME_HEIGHT, &width,
1066 &height);
1067 if (!*output || (*output)->w != width || (*output)->h != height) {
1068 image_t *replacement = image_new(width, height);
1069 if (!replacement)
1070 return SET_ERRNO(ERROR_MEMORY, "Cannot reserve transmission image workspace");
1071 image_destroy(*output);
1072 *output = replacement;
1073 }
1074 if (frame != *output) {
1075 if (frame->w == width && frame->h == height)
1076 memcpy((*output)->pixels, frame->pixels, (size_t)width * (size_t)height * sizeof(rgb_pixel_t));
1077 else
1078 image_resize(frame, *output);
1079 }
1080 return ASCIICHAT_OK;
1081}
1082
1084 image_t *output = NULL;
1086 return NULL;
1087 return output;
1088}
1089
1091 if (!ctx || !ctx->initialized || !ctx->source) {
1092 return;
1093 }
1094
1095 uint64_t frame_interval_ns = NS_PER_SEC_INT / ctx->target_fps;
1096 if (frame_interval_ns == 0) {
1097 return;
1098 }
1099
1100 uint64_t now_ns = time_get_ns();
1101 ctx->next_frame_deadline_ns = capture_pacing_deadline(ctx->next_frame_deadline_ns, now_ns, frame_interval_ns);
1102 if (ctx->next_frame_deadline_ns > now_ns)
1103 platform_sleep_ns(ctx->next_frame_deadline_ns - now_ns);
1104}
1105
1107 if (!ctx || !ctx->initialized || !ctx->source) {
1108 return true;
1109 }
1110
1111 bool pending = ctx->loader_created;
1112 size_t next;
1113 if (pending || (!ctx->advance_failed && (media_playlist_adjacent(&ctx->playlist, 1, &next) ||
1114 (GET_OPTION(media_loop) && ctx->playlist.count > 1))))
1115 return false;
1116 if (media_source_get_type(ctx->source) == MEDIA_SOURCE_FILE && !GET_OPTION(snapshot_mode) &&
1117 !GET_OPTION(render_file)[0])
1118 return false;
1119 return media_source_at_end(ctx->source);
1120}
1121
1123 return ctx != NULL && ctx->initialized && ctx->source != NULL;
1124}
1125
1127 if (!ctx || !ctx->initialized || ctx->frame_count == 0) {
1128 return 0.0;
1129 }
1130
1131 // Calculate elapsed time in nanoseconds then convert to seconds
1132 uint64_t elapsed_ns = time_elapsed_ns(ctx->start_time_ns, time_get_ns());
1133 double elapsed_sec = time_ns_to_s(elapsed_ns);
1134
1135 if (elapsed_sec <= 0.0) {
1136 return 0.0;
1137 }
1138
1139 return (double)ctx->frame_count / elapsed_sec;
1140}
1141
1143 if (!ctx) {
1144 return 0;
1145 }
1146 return ctx->target_fps;
1147}
1148
1150 if (!ctx || !ctx->initialized) {
1151 return false;
1152 }
1153 return ctx->audio_enabled;
1154}
1155
1156static size_t capture_read_audio(session_capture_ctx_t *ctx, float *buffer, size_t num_samples) {
1157 if (!ctx || !ctx->initialized || !buffer || num_samples == 0) {
1158 return 0;
1159 }
1160
1161 if (!ctx->audio_enabled) {
1162 return 0;
1163 }
1164
1165 // If using file audio, read from media source
1166 if (ctx->using_file_audio && ctx->source && !atomic_load_bool(&ctx->non_media_active)) {
1167 return media_source_read_audio(ctx->source, buffer, num_samples);
1168 }
1169
1170 // If using microphone fallback, read from mic context
1171 void *current_audio_ctx = atomic_ptr_load(&ctx->audio_ctx);
1172 if (ctx->mic_audio_ctx || (current_audio_ctx && atomic_load_bool(&ctx->non_media_active))) {
1173 audio_context_t *audio_ctx = ctx->mic_audio_ctx ? ctx->mic_audio_ctx : current_audio_ctx;
1174 if (audio_ctx && audio_ctx->capture_buffer) {
1175 return audio_ring_buffer_read(audio_ctx->capture_buffer, buffer, num_samples);
1176 }
1177 }
1178
1179 return 0;
1180}
1181
1182size_t session_capture_read_audio(session_capture_ctx_t *ctx, float *buffer, size_t num_samples) {
1183 ASCII_OTEL_SCOPE(SESSION_CAPTURE_READ_AUDIO);
1185 size_t count = capture_read_audio(ctx, buffer, num_samples);
1187 return count;
1188}
1189
1191 if (!ctx || !ctx->initialized) {
1192 return false;
1193 }
1194 return ctx->using_file_audio;
1195}
1196
1198 if (!ctx || !ctx->initialized || !ctx->source) {
1199 return NULL;
1200 }
1201 return (void *)ctx->source;
1202}
1203
1205 if (!ctx || !ctx->initialized) {
1206 return NULL;
1207 }
1208 return atomic_ptr_load(&ctx->audio_ctx);
1209}
1210
1212 void *user_data) {
1213 if (ctx) {
1214 ctx->audio_update = update;
1215 ctx->audio_update_data = user_data;
1216 }
1217}
1218
1220 if (ctx) {
1222 atomic_ptr_store(&ctx->audio_ctx, audio_ctx);
1224 }
1225}
1226
1228 if (!ctx || !ctx->source || media_source_get_type(ctx->source) != MEDIA_SOURCE_FILE)
1229 return;
1230 // Swap UI preferences in input order so a following animation key applies to
1231 // the newly selected mode even before the capture thread switches devices.
1233 options_set_bool("media_input", !ctx->non_media_requested);
1234 if (ctx->non_media_requested) {
1235 options_set_bool("matrix_rain", ctx->saved_matrix);
1236 options_set_bool("waveform", ctx->saved_waveform);
1237 options_set_bool("fft", ctx->saved_fft);
1238 } else {
1239 ctx->saved_matrix = GET_OPTION(matrix_rain);
1240 ctx->saved_waveform = GET_OPTION(waveform);
1241 ctx->saved_fft = GET_OPTION(fft);
1242 options_set_bool("matrix_rain", false);
1243 options_set_bool("waveform", false);
1244 options_set_bool("fft", false);
1245 }
1246 atomic_fetch_add_u64(&ctx->toggle_media, 1);
1247}
1248
1250 return ctx && !atomic_load_bool(&ctx->non_media_active);
1251}
1252
1254 if (ctx)
1255 atomic_store_bool(&ctx->redraw_media, true);
1256}
1257
1259 return ctx ? atomic_load_u64(&ctx->source_epoch) : 0;
1260}
1262 return ctx ? atomic_load_u64(&ctx->frame_serial) : 0;
1263}
1265 if (!ctx || !atomic_load_bool(&ctx->awaiting_presentation))
1266 return;
1267 mutex_lock(&ctx->load_mutex);
1268 if (epoch == session_capture_source_epoch(ctx) && atomic_exchange_bool(&ctx->awaiting_presentation, false))
1270 mutex_unlock(&ctx->load_mutex);
1271}
bool errno_peek_since(asciichat_errno_scope_t scope, asciichat_error_context_t *out)
void errno_scope_end(asciichat_errno_scope_t scope, asciichat_errno_outcome_t outcome)
asciichat_errno_scope_t errno_checkpoint(void)
asciichat_errno_scope_t errno_scope_begin(void)
Synchronized per-thread pending error stacks and value snapshots. SET_ERRNO pushes,...
#define SET_ERRNO(code,...)
#define CLEAR_ERRNO()
@ ASCIICHAT_ERRNO_DISMISSED
#define LOG_ERRNO_IF_SET(message)
#define CAPTURE_TRACKER_NAME_BUFFER_SIZE
#define SESSION_CAPTURE_FAILURE_BUFFER_SIZE
void session_capture_lock_source(session_capture_ctx_t *ctx)
void session_capture_unlock_source(session_capture_ctx_t *ctx)
void session_capture_set_audio_context(session_capture_ctx_t *ctx, void *audio_ctx)
Set the audio context for playback control.
void session_capture_set_audio_updater(session_capture_ctx_t *ctx, session_capture_audio_update_fn update, void *user_data)
void session_capture_toggle_media(session_capture_ctx_t *ctx)
#define SESSION_CAPTURE_LOAD_ERROR_BUFFER_SIZE
#define SESSION_CAPTURE_ERROR_BUFFER_SIZE
void session_capture_skip_media(session_capture_ctx_t *ctx, int direction)
uint64_t session_capture_frame_serial(session_capture_ctx_t *ctx)
bool session_capture_media_active(session_capture_ctx_t *ctx)
void * session_capture_get_audio_context(session_capture_ctx_t *ctx)
Get the audio context for playback control.
#define SESSION_MAX_FRAME_WIDTH
Maximum frame width for network transmission (bandwidth optimization).
asciichat_error_t session_capture_open_playlist(session_capture_ctx_t *ctx, const char *text)
#define SESSION_CAPTURE_LOAD_PATH_BUFFER_SIZE
void session_capture_redraw_media(session_capture_ctx_t *ctx)
void session_capture_complete_open(session_capture_ctx_t *ctx, uint64_t epoch)
asciichat_error_t session_capture_open_media(session_capture_ctx_t *ctx, const char *path)
#define SESSION_CAPTURE_WEBCAM_INDEX_BUFFER_SIZE
#define SESSION_CAPTURE_PATH_BUFFER_SIZE
#define SESSION_MAX_FRAME_HEIGHT
Maximum frame height for network transmission (bandwidth optimization).
uint64_t session_capture_source_epoch(session_capture_ctx_t *ctx)
bool session_capture_cancel_open(session_capture_ctx_t *ctx)
asciichat_error_t session_capture_process_for_transmission_into(session_capture_ctx_t *ctx, image_t *frame, image_t **output)
📹 Unified media capture abstraction for session-based video sources
void *(* session_capture_audio_update_fn)(media_source_t *source, void *user_data)
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Named object registry for debugging — log identifiable resource names.
image_t * face_follow_image_process(face_follow_image_t **context, image_t *source, double now, bool enabled, bool debug, int max_width, int max_height)
void face_follow_image_destroy(face_follow_image_t *context)
struct face_follow_image face_follow_image_t
audio_visualization_source_t source
Definition fft.c:24
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
⏱️ FPS tracking utility for monitoring frame throughput across all threads
Buffered terminal frame rendering.
bool audio_should_enable_microphone(audio_capture_source_t source, bool has_media_audio)
Determine if microphone should be enabled based on audio source setting and media state.
size_t audio_ring_buffer_read(audio_ring_buffer_t *rb, float *data, size_t samples)
Read audio samples from ring buffer.
asciichat_error_t audio_start_duplex(audio_context_t *ctx)
Start full-duplex audio (simultaneous capture and playback).
asciichat_error_t audio_stop_duplex(audio_context_t *ctx)
Stop full-duplex audio.
void audio_flush_playback_buffers(audio_context_t *ctx)
Flush all audio buffers to synchronize with seek operations.
void audio_ring_buffer_clear(audio_ring_buffer_t *rb)
Clear all audio samples from ring buffer.
unsigned int uint32_t
Definition common.h:62
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
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
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
unsigned long long uint64_t
Definition common.h:63
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 NAMED_UNREGISTER(ptr)
Unregister a pointer.
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_INVALID_STATE
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_MEDIA_INIT
Definition error_codes.h:65
@ ERROR_INVALID_PARAM
@ ERROR_THREAD
#define DEFAULT_MAX_FPS
Default maximum frame rate (frames per second).
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
bool media_source_at_end(media_source_t *source)
Check if media source reached end of stream.
Definition source.c:836
bool media_source_is_paused(media_source_t *source)
Check if media source is paused.
Definition source.c:1088
bool media_source_has_video(media_source_t *source)
Check if media source has video stream.
Definition source.c:699
size_t media_source_read_audio(media_source_t *source, float *buffer, size_t num_samples)
Read audio samples from media source.
Definition source.c:722
bool media_source_has_audio(media_source_t *source)
Check if media source has audio stream.
Definition source.c:801
double media_source_get_position(media_source_t *source)
Get current playback position in seconds.
Definition source.c:1025
void media_source_destroy(media_source_t *source)
Destroy media source and free resources.
Definition source.c:508
void media_source_resume(media_source_t *source)
Resume media playback after pause.
Definition source.c:1081
double media_source_get_video_fps(media_source_t *source)
Get video frame rate in frames per second.
Definition source.c:1047
double media_source_get_duration(media_source_t *source)
Get media duration in seconds.
Definition source.c:1003
void media_source_pause(media_source_t *source)
Pause media playback.
Definition source.c:1074
image_t * media_source_read_video(media_source_t *source)
Read next video frame from media source.
Definition source.c:592
asciichat_error_t media_source_seek(media_source_t *source, double timestamp_sec)
Seek media source to timestamp.
Definition source.c:917
void media_source_set_exit_callback(media_source_t *source, bool(*should_exit_callback)(void *), void *user_data)
Set exit signal callback for graceful shutdown during I/O.
Definition source.c:1110
media_source_type_t media_source_get_type(media_source_t *source)
Get media source type.
Definition source.c:999
void media_source_set_audio_context(media_source_t *source, void *audio_ctx)
Set audio context for playback buffer management.
Definition source.c:1104
media_source_t * media_source_create(media_source_type_t type, const char *path)
Create a new media source.
Definition source.c:221
void media_source_set_loop(media_source_t *source, bool loop)
Enable or disable looping.
Definition source.c:824
@ MEDIA_SOURCE_STDIN
Piped or redirected input.
Definition source.h:100
@ MEDIA_SOURCE_WEBCAM
Hardware webcam device.
Definition source.h:98
@ MEDIA_SOURCE_FILE
Media file (video/audio).
Definition source.h:99
@ MEDIA_SOURCE_TEST
Test pattern generator.
Definition source.h:101
#define NS_PER_SEC_INT
Definition time.h:169
#define NS_PER_MS
Definition time.h:159
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition time.c:123
#define US_PER_SEC_INT
Definition time.h:173
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
struct options_state options_t
Immutable, RCU-published snapshot of the application's runtime configuration.
asciichat_error_t options_set_string(const char *name, const char *value)
Definition rcu.c:908
asciichat_error_t options_set_bool(const char *name, bool value)
Definition rcu.c:902
@ AUDIO_CAPTURE_SOURCE_MIC
Definition options.h:292
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
bool session_capture_has_audio(session_capture_ctx_t *ctx)
Check if capture source has audio available.
bool(* session_capture_should_exit_fn)(void *user_data)
Callback type to check if initialization should be cancelled.
image_t * session_capture_process_for_transmission(session_capture_ctx_t *ctx, image_t *frame)
Process a frame for network transmission (resize if needed).
session_capture_ctx_t * session_mirror_capture_create(const session_capture_config_t *config)
Create a new session capture context.
bool session_capture_at_end(session_capture_ctx_t *ctx)
Check if capture source has reached end of stream.
double session_capture_get_current_fps(session_capture_ctx_t *ctx)
Get the current FPS being achieved by the capture source.
size_t session_capture_read_audio(session_capture_ctx_t *ctx, float *buffer, size_t num_samples)
Read audio samples from capture source.
struct session_capture_ctx session_capture_ctx_t
session capture config keeps the values that move through the subsystem: type, path,...
image_t * session_capture_read_frame(session_capture_ctx_t *ctx)
Read the next video frame from the capture source.
bool session_capture_is_valid(session_capture_ctx_t *ctx)
Check if capture context is initialized and valid.
bool session_capture_using_file_audio(session_capture_ctx_t *ctx)
Check if currently using file audio vs microphone fallback.
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.
session_capture_ctx_t * session_network_capture_create(uint32_t target_fps)
Create network mode capture context without media source.
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.
bool shutdown_is_requested(void)
Check if shutdown has been requested.
Definition common.c:86
void image_resize(const image_t *s, image_t *d)
Resize image using nearest-neighbor interpolation.
void image_destroy(image_t *image)
Destroy an image allocated with image_new().
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
🔊 Audio Capture and Playback Interface for ascii-chat
void stats_counter_add(stats_scope_t *scope, stats_counter_id_t id, uint64_t amount)
void stats_duration_record(stats_scope_t *scope, stats_duration_id_t id, uint64_t elapsed_ns)
📝 Logging API with multiple log levels and terminal output control
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:746
#define NOTICE(severity, title,...)
Definition notice.h:68
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
void ascii_otel_ui_usage(const char *screen, const char *event, uint64_t duration_ns)
Definition otel.c:434
void ascii_otel_error(const char *message)
Definition otel.c:804
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
#define ASCII_OTEL_SCOPE_PARENT(id, parent)
Definition otel.h:69
#define ASCII_OTEL_ATTR_STRING(key, value)
Definition otel.h:66
asciichat_error_t media_playlist_append(media_playlist_t *list, const char *path, bool url)
Definition playlist.c:7
asciichat_error_t media_playlist_parse(const char *text, media_playlist_t *list)
Definition playlist.c:32
bool media_playlist_adjacent(const media_playlist_t *list, int direction, size_t *index)
Definition playlist.c:113
asciichat_error_t ui_media_loading_present(void)
Definition prompt.c:332
static bool size_t index
Definition queries.h:8
void stats_runtime_media(unsigned width, unsigned height, double position, double duration)
Definition runtime.c:69
stats_scope_t * stats_runtime_scope(void)
Definition runtime.c:80
Shutdown check system for clean library/application separation.
media_source_t * media_source_create_interruptible(media_source_type_t type, const char *path, bool(*cancel)(void *), void *data)
Definition source.c:225
bool media_source_uses_webcam(media_source_t *source)
Definition source.c:587
asciichat_error_t media_source_start_video(media_source_t *source)
Definition source.c:571
🎬 Unified media source abstraction for webcam, files, and stdin
🔤 String Manipulation and Shell Escaping Utilities
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...
When code needs to work with audio device info, it keeps index, max_input_channels,...
bool monitor_local_media
Play local file audio in solo sessions.
void * capture_media_source
Borrowed local media audio; configure before starting duplex.
audio_ring_buffer_t * capture_buffer
OLD: Worker → Network (will become processed_capture_rb).
bool playback_only
True for playback-only mode (mirror), skip microphone capture.
Counters and timestamps used to calculate and report frame rate.
Definition fps.h:53
const char * tracker_name
Definition fps.h:59
SIMD-friendly storage for one RGB pixel. r, g, and b hold the color channels and padding keeps the la...
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).
char paths[64][256]
Definition playlist.h:10
Media source for video and audio capture.
Definition source.c:47
Media-source and loader settings used to create a 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).
session_capture_should_exit_fn should_exit_callback
Optional cancellation callback, also invoked by media loader threads. The callback and its data must ...
void * callback_data
Opaque data passed to should_exit_callback.
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).
Runtime state for media loading, frame timing, and source switching.
fps_t fps_tracker
FPS tracker for monitoring capture rate.
media_source_t * saved_audio_source
session_capture_should_exit_fn should_exit_callback
void * mic_audio_ctx
Microphone audio context for fallback (borrowed, not owned).
bool file_has_audio
File has audio stream available.
media_playlist_t load_playlist
session_capture_audio_update_fn audio_update
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.
_Atomic bool audio_enabled
Audio is enabled for capture.
media_source_t * loaded_source
uint32_t target_fps
Target frames per second.
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).
face_follow_image_t * face_follow
uint64_t next_frame_deadline_ns
Next capture deadline on the monotonic clock (nanoseconds).
atomic_ptr_t 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.
test pattern is intentionally a small, concrete record. Fields such as image, rgba give callers the i...
⏱️ High-precision timing utilities using sokol_time.h and uthash
void ui_controller_remove(ui_screen_t screen)
void ui_controller_set_effective_fps(unsigned int fps)
@ UI_SCREEN_RENDER_PROGRESS