ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
Session Management Module

Real-time audio-video session abstraction layer. More...

Files

file  splash.c
 Intro and status screen display implementation with animated rainbow effects.
file  update_banner.c
 Update available prompt screen implementation.
file  fps_counter.c
 FPS counter implementation using rolling-window timestamp buffer.
file  audio.c
 ๐Ÿ”Š Session-level audio coordination implementation
file  audio.h
 ๐Ÿ”Š Session-level audio coordination wrapper
file  capture.c
 ๐Ÿ“น Unified media capture implementation
file  capture.h
 ๐Ÿ“น Unified media capture abstraction for session-based video sources
file  client_like.c
file  client_like.h
file  consensus.c
 Mode-agnostic ring consensus abstraction implementation.
file  consensus.h
 Mode-agnostic ring consensus abstraction for session discovery.
file  consensus_integration.c
 Integration helpers for session consensus in discovery mode.
file  consensus_integration.h
 Integration helpers for session consensus across modes.
file  display.c
 ๐Ÿ–ฅ๏ธ Unified terminal display implementation
file  display.h
 ๐Ÿ–ฅ๏ธ Unified terminal display abstraction for session-based rendering
file  host.c
 ๐Ÿ  Server-side session hosting implementation
file  host.h
 ๐Ÿ  Server-side session hosting abstraction
file  keyboard_handler.c
 Keyboard input handler implementation.
file  keyboard_handler.h
 ๐ŸŽฎ Keyboard input handler for interactive session controls
file  participant.c
 ๐Ÿ‘ค Client-side session participation implementation
file  participant.h
 ๐Ÿ‘ค Client-side session participation abstraction
file  pipeline.c
 Three-thread render pipeline implementation.
file  pipeline.h
 Three-thread render pipeline: capture โ†’ display/encode.
file  render.c
 Unified render loop implementation for all display modes.
file  render.h
 Centralized render loop abstraction for all display modes.
file  server_like.c
file  session_log_buffer.h
 Thread-safe circular log buffer for session screens (status, splash).
file  keyboard_help.h
 ๐Ÿ†˜ Interactive keyboard help overlay for session keyboard shortcuts
file  splash.h
 Intro and status screen display for ascii-chat with animated rainbow effects.
file  status.h
 ๐Ÿ“Š Status screen display and management for server/discovery service modes
file  update_banner.h
 Update available prompt screen shown after splash.
file  fps_counter.h
 โฑ๏ธ FPS counter for measuring output write throughput

Data Structures

struct  session_capture_config_t
 Media-source and loader settings used to create a session capture context. More...
struct  session_display_config_t
 Terminal-rendering and snapshot settings used to create a display context. More...
struct  session_host_client_info_t
 Snapshot of the address, identity, activity, and connection time for one host client. More...
struct  session_host_callbacks_t
 Snapshot of the address, identity, activity, and connection time for one host client. More...
struct  session_host_config_t
 Host-side event hooks for joins, departures, media, and errors. More...
struct  session_participant_callbacks_t
 Client-side event hooks for connection, media, settings, and errors. More...
struct  session_participant_config_t
 Client-side event hooks for connection, media, settings, and errors. More...

Typedefs

typedef struct session_audio_ctx session_audio_ctx_t
 Opaque session audio context handle.
typedef bool(* session_capture_should_exit_fn) (void *user_data)
 Callback type to check if initialization should be cancelled.
typedef struct session_capture_ctx session_capture_ctx_t
 session capture config keeps the values that move through the subsystem: type, path, target_fps, loop. Some members are pointers into caller- or subsystem-owned memory, so lifetime follows the API that populated them rather than the struct copy itself.
typedef bool(* session_display_should_exit_fn) (void *user_data)
 Callback type to check if initialization should be cancelled.
typedef struct session_display_ctx session_display_ctx_t
 session display config is intentionally a small, concrete record. Fields such as snapshot_mode, palette_type, custom_palette, color_mode give callers the identity and current value they need. It is normally borrowed from, or owned by, the surrounding subsystemโ€”not independently managed.
typedef struct session_host session_host_t
 Opaque session host handle.
typedef struct acip_transport acip_transport_t
 Forward declaration of ACIP transport.
typedef struct session_display_ctx session_display_ctx_t
 Runtime terminal state and reusable workspaces for ASCII rendering.
typedef struct session_participant session_participant_t
 Client session state spanning connection, transport, media workers, and callbacks.
typedef struct acip_transport acip_transport_t
 Forward declaration of ACIP transport.
typedef bool(* session_should_exit_fn) (void *user_data)
 Exit condition callback type.
typedef image_t *(* session_capture_fn) (void *user_data)
 Capture callback for event-driven frame source.
typedef void(* session_sleep_for_frame_fn) (void *user_data)
 Sleep callback for custom frame timing.
typedef void(* session_keyboard_handler_fn) (session_capture_ctx_t *capture, int key, void *user_data)
 Keyboard input handler callback type.
typedef struct session_display_ctx session_display_ctx_t
typedef struct fps_counter_s fps_counter_t
 Opaque FPS counter handle.

Functions

session_audio_ctx_t * session_audio_create (bool is_host)
 Create a new session audio context.
void session_audio_destroy (session_audio_ctx_t *ctx)
 Destroy session audio context and free resources.
asciichat_error_t session_audio_start_capture (session_audio_ctx_t *ctx)
 Start audio capture (microphone input).
asciichat_error_t session_audio_start_playback (session_audio_ctx_t *ctx)
 Start audio playback (speaker output).
asciichat_error_t session_audio_start_duplex (session_audio_ctx_t *ctx)
 Start full-duplex audio (simultaneous capture and playback).
void session_audio_stop (session_audio_ctx_t *ctx)
 Stop all audio streams.
bool session_audio_is_running (session_audio_ctx_t *ctx)
 Check if audio is currently running.
size_t session_audio_read_captured (session_audio_ctx_t *ctx, float *buffer, size_t num_samples)
 Read captured audio samples.
asciichat_error_t session_audio_write_playback (session_audio_ctx_t *ctx, const float *buffer, size_t num_samples)
 Write audio samples for playback.
asciichat_error_t session_audio_add_source (session_audio_ctx_t *ctx, uint32_t source_id)
 Add an audio source for mixing (host only).
void session_audio_remove_source (session_audio_ctx_t *ctx, uint32_t source_id)
 Remove an audio source from mixing (host only).
asciichat_error_t session_audio_write_source (session_audio_ctx_t *ctx, uint32_t source_id, const float *samples, size_t num_samples)
 Write audio samples from a specific source (host only).
size_t session_audio_mix_excluding (session_audio_ctx_t *ctx, uint32_t exclude_id, float *output, size_t num_samples)
 Mix all sources except one and return the result (host only).
session_capture_ctx_t * session_mirror_capture_create (const session_capture_config_t *config)
 Create a new session capture context.
session_capture_ctx_t * session_network_capture_create (uint32_t target_fps)
 Create network mode capture context without media source.
session_capture_ctx_t * session_capture_create (const session_capture_config_t *config)
 Legacy function - creates either mirror or network capture based on options.
void session_capture_destroy (session_capture_ctx_t *ctx)
 Destroy session capture context and free resources.
image_t * session_capture_read_frame (session_capture_ctx_t *ctx)
 Read the next video frame from the capture source.
image_t * session_capture_process_for_transmission (session_capture_ctx_t *ctx, image_t *frame)
 Process a frame for network transmission (resize if needed).
void session_capture_sleep_for_fps (session_capture_ctx_t *ctx)
 Sleep to maintain target frame rate.
bool session_capture_at_end (session_capture_ctx_t *ctx)
 Check if capture source has reached end of stream.
bool session_capture_is_valid (session_capture_ctx_t *ctx)
 Check if capture context is initialized and valid.
double session_capture_get_current_fps (session_capture_ctx_t *ctx)
 Get the current FPS being achieved by the capture source.
uint32_t session_capture_get_target_fps (session_capture_ctx_t *ctx)
 Get the target FPS configured for this capture context.
bool session_capture_has_audio (session_capture_ctx_t *ctx)
 Check if capture source has audio available.
size_t session_capture_read_audio (session_capture_ctx_t *ctx, float *buffer, size_t num_samples)
 Read audio samples from capture source.
bool session_capture_using_file_audio (session_capture_ctx_t *ctx)
 Check if currently using file audio vs microphone fallback.
void * session_capture_get_media_source (session_capture_ctx_t *ctx)
 Get the underlying media source from capture context.
terminal_fd_reader_t * session_client_like_get_stdin_reader (void)
session_display_ctx_t * session_display_create (const session_display_config_t *config)
 Create a new session display context.
session_display_ctx_t * session_display_get_global_context (void)
 Get the global display context (used for signal handlers and special cleanup).
void session_display_set_global_context_public (session_display_ctx_t *ctx)
 Set the global display context (public setter for discovery cleanup).
asciichat_error_t session_display_finish_render_file (session_display_ctx_t *ctx)
 Destroy session display context and free resources.
void session_display_begin_drain (session_display_ctx_t *ctx, uint64_t total, bool total_known)
void session_display_destroy (session_display_ctx_t *ctx)
void session_display_set_stdin_reader (session_display_ctx_t *ctx, void *reader)
 Set stdin frame reader for ASCII-to-video rendering.
bool session_display_has_tty (session_display_ctx_t *ctx)
 Check if display has a TTY (terminal) available.
const terminal_capabilities_t * session_display_get_caps (session_display_ctx_t *ctx)
 Get detected terminal capabilities.
const char * session_display_get_palette_chars (session_display_ctx_t *ctx)
 Get the palette characters string.
size_t session_display_get_palette_len (session_display_ctx_t *ctx)
 Get the palette character count.
const char * session_display_get_luminance_palette (session_display_ctx_t *ctx)
 Get the luminance mapping palette.
int session_display_get_tty_fd (session_display_ctx_t *ctx)
 Get the TTY file descriptor.
void * session_display_get_stdin_reader (session_display_ctx_t *ctx)
 Get the stdin frame reader for ASCII-to-video rendering.
bool session_display_has_render_file (session_display_ctx_t *ctx)
 Check if display has render-file configured.
uint32_t session_display_get_render_fps (session_display_ctx_t *ctx)
 Get the render FPS configured for file output.
void session_display_set_render_fps (session_display_ctx_t *ctx, uint32_t fps)
 Set the render FPS for file output encoding.
bool session_display_has_first_frame (session_display_ctx_t *ctx)
 Check if the display has rendered its first frame.
void session_display_reset_first_frame (session_display_ctx_t *ctx)
 Reset the first frame flag to allow splash screen to show again on next render.
char * session_display_convert_to_ascii (session_display_ctx_t *ctx, const image_t *image)
 Convert an image to ASCII art using display context and command-line options.
void session_display_write_ascii (session_display_ctx_t *ctx, const char *frame_data)
 Write ASCII frame to terminal only (no encoding).
asciichat_error_t session_display_encode_frame (session_display_ctx_t *ctx, const image_t *image, uint64_t captured_ns)
 Encode frame to render-file (FFmpeg only, no terminal output).
void session_display_render_frame (session_display_ctx_t *ctx, const char *frame_data)
 Render an ASCII frame to the terminal.
void session_display_write_raw (session_display_ctx_t *ctx, const char *data, size_t len)
 Render raw bytes to the terminal without frame processing.
void session_display_set_snapshot_actual_duration (session_display_ctx_t *ctx, double actual_duration_sec)
 Set the actual wall-clock duration for snapshot mode frame timing.
void session_display_reset (session_display_ctx_t *ctx)
 Reset terminal to default state.
void session_display_clear (session_display_ctx_t *ctx)
 Clear the terminal screen.
void session_display_cursor_home (session_display_ctx_t *ctx)
 Move cursor to home position (top-left).
bool session_display_has_audio_playback (session_display_ctx_t *ctx)
 Check if display has audio playback configured.
asciichat_error_t session_display_write_audio (session_display_ctx_t *ctx, const float *buffer, size_t num_samples)
 Write audio samples to playback buffer.
session_host_t * session_host_create (const session_host_config_t *config)
 Create a new session host.
asciichat_error_t session_host_destroy (session_host_t *host)
 Destroy session host and free resources.
asciichat_error_t session_host_start (session_host_t *host)
 Start accepting client connections.
asciichat_error_t session_host_stop (session_host_t *host)
 Stop accepting connections and disconnect all clients.
bool session_host_is_running (session_host_t *host)
 Check if host is running.
uint32_t session_host_add_memory_participant (session_host_t *host)
 Add a memory participant (host's own media).
asciichat_error_t session_host_inject_frame (session_host_t *host, uint32_t participant_id, const image_t *frame)
 Inject a video frame from memory participant.
asciichat_error_t session_host_inject_audio (session_host_t *host, uint32_t participant_id, const float *samples, size_t count)
 Inject audio samples from memory participant.
asciichat_error_t session_host_remove_client (session_host_t *host, uint32_t client_id)
 Remove a client by ID.
asciichat_error_t session_host_find_client (session_host_t *host, uint32_t client_id, session_host_client_info_t *info)
 Find a client by ID.
int session_host_get_client_count (session_host_t *host)
 Get number of connected clients.
int session_host_get_client_ids (session_host_t *host, uint32_t *ids, int max_ids)
 Get list of all connected client IDs.
asciichat_error_t session_host_broadcast_frame (session_host_t *host, const char *frame)
 Broadcast ASCII frame to all clients.
asciichat_error_t session_host_send_frame (session_host_t *host, uint32_t client_id, const char *frame)
 Send ASCII frame to a specific client.
asciichat_error_t session_host_set_display (session_host_t *host, struct session_display_ctx *display)
 Set a native terminal display context for local frame rendering.
asciichat_error_t session_host_start_render (session_host_t *host)
 Start media rendering thread (video mixing and audio distribution).
void session_host_stop_render (session_host_t *host)
 Stop media rendering thread.
asciichat_error_t session_host_set_client_transport (session_host_t *host, uint32_t client_id, acip_transport_t *transport, acip_transport_ownership_t ownership)
 Select a transport for a specific client in the host.
acip_transport_t * session_host_get_client_transport (session_host_t *host, uint32_t client_id)
 Get the current transport for a specific client in the host.
bool session_host_client_has_transport (session_host_t *host, uint32_t client_id)
 Check if a specific client has an active ACIP transport.
void session_handle_keyboard_input (session_capture_ctx_t *capture, session_display_ctx_t *display, keyboard_key_t key)
 Handle keyboard input in a session.
session_participant_t * session_participant_create (const session_participant_config_t *config)
 Server address, security, media, transport, and callback settings for a participant.
void session_participant_destroy (session_participant_t *p)
 Destroy session participant and free resources.
asciichat_error_t session_participant_connect (session_participant_t *p)
 Connect to session server.
void session_participant_disconnect (session_participant_t *p)
 Disconnect from session server.
bool session_participant_is_connected (session_participant_t *p)
 Check if participant is connected.
uint32_t session_participant_get_client_id (session_participant_t *p)
 Get assigned client ID.
asciichat_error_t session_participant_start_video (session_participant_t *p)
 Start video capture and streaming.
void session_participant_stop_video (session_participant_t *p)
 Stop and join video capture, then release its resources Also stops audio when it may borrow the video media source.
bool session_participant_is_video_active (session_participant_t *p)
 Check if video is streaming.
asciichat_error_t session_participant_start_audio (session_participant_t *p)
 Start audio capture and streaming.
void session_participant_stop_audio (session_participant_t *p)
 Stop audio capture and streaming.
bool session_participant_is_audio_active (session_participant_t *p)
 Check if audio is streaming.
asciichat_error_t session_participant_get_settings (session_participant_t *p, session_settings_t *settings)
 Get current session settings.
asciichat_error_t session_participant_request_settings (session_participant_t *p, const session_settings_t *settings)
 Request this participant's remote rendering update.
asciichat_error_t session_participant_set_transport (session_participant_t *p, acip_transport_t *transport, acip_transport_ownership_t ownership)
 Select a transport and its ownership mode for the participant.
acip_transport_t * session_participant_get_transport (session_participant_t *p)
 Get the current transport for the participant.
bool session_participant_has_transport (session_participant_t *p)
 Check if participant has a selected transport.
asciichat_error_t session_render_loop (session_capture_ctx_t *capture, session_display_ctx_t *display, session_should_exit_fn should_exit, session_capture_fn capture_cb, session_sleep_for_frame_fn sleep_cb, session_keyboard_handler_fn keyboard_handler, void *user_data)
 Unified render loop for all display modes.
void keyboard_help_toggle (session_display_ctx_t *ctx)
 Toggle keyboard help on/off.
void keyboard_help_toggle_terminal (int fd)
int keyboard_help_parent_screen (void)
const char * keyboard_help_screen_name (int parent)
bool keyboard_help_is_active (session_display_ctx_t *ctx)
 Check if keyboard help is currently active.
void keyboard_help_render (session_display_ctx_t *ctx)
 Render keyboard help TUI overlay.
void session_display_set_global_context (session_display_ctx_t *ctx)
 Set the global display context (for signal handlers like Ctrl+C).
bool keyboard_help_is_active_global (void)
 Check if keyboard help is active (global accessor for signal handlers).
void keyboard_help_toggle_global (void)
 Toggle keyboard help (global accessor for signal handlers).
void keyboard_help_signal_cancel (void)
 Request help screen closure from signal handler (Ctrl+C).
bool keyboard_help_check_signal_cancel (void)
 Check if help screen closure was requested by signal handler.
void splash_log_init (void)
 Initialize log buffer for splash animation.
void splash_log_destroy (void)
 Destroy splash log buffer.
void splash_log_clear (void)
 Clear splash log buffer.
void splash_log_append (const char *message)
 Append message to splash log buffer.
int splash_intro_start (session_display_ctx_t *ctx)
 Start animated intro screen with rainbow ASCII art (non-blocking).
int splash_intro_done (void)
 Signal that intro splash should end (first frame ready to render).
void splash_wait_for_animation (void)
 Wait for splash animation thread to fully exit.
bool splash_should_display (bool is_intro)
 Check if splash screen should display.
void splash_restore_stderr (void)
 Restore stderr after splash animation completes.
void splash_set_update_notification (const char *notification)
 Set update notification to display on splash/status screens.
void splash_clear_display_context (void)
 Clear the cached display context to prevent use-after-free.
void splash_notify_first_frame (void)
 Notify splash that the first frame has been rendered.
bool splash_is_running (void)
 Check if splash screen animation is currently running.
void update_banner_set_result (const update_check_result_t *result)
 Store the update check result (thread-safe).
bool update_banner_has_update (void)
 Check if an update is available (thread-safe).
bool update_banner_show_prompt (struct session_display_ctx *ctx)
 Show the update prompt screen and wait for user input.
void update_banner_print_instructions (void)
 Print upgrade instructions to stdout and prepare for clean exit.
bool update_banner_install_update (void)
void update_banner_start_check (void)
 Start the background update check thread.
void update_banner_wait_for_check (void)
 Wait for the background update check thread to finish.
void update_banner_cancel_check (void)
void update_banner_toggle_overlay (void)
bool update_banner_handle_key (int key)
bool update_banner_is_checking (void)
bool update_banner_is_overlay_open (void)
fps_counter_t * fps_counter_create (void)
 Create a new FPS counter.
void fps_counter_destroy (fps_counter_t *counter)
 Destroy FPS counter and free resources.
void fps_counter_tick (fps_counter_t *counter)
 Record a frame timestamp.
float fps_counter_get (fps_counter_t *counter)
 Get the current FPS value.
void fps_counter_reset (fps_counter_t *counter)
void fps_counter_tick_at (fps_counter_t *counter, uint64_t now)
float fps_counter_get_at (fps_counter_t *counter, uint64_t now)
bool fps_counter_set_visible (fps_counter_t *counter, bool visible)
void fps_counter_set_effective_fps (fps_counter_t *counter, unsigned int fps)
void fps_counter_frame_begin (fps_counter_t *counter, bool measure)
void fps_counter_write_begin (fps_counter_t *counter)
void fps_counter_write_end (fps_counter_t *counter, bool complete)
void fps_counter_frame_end (fps_counter_t *counter, uint64_t now)
void fps_counter_render (fps_counter_t *counter, int fd, int columns, bool redrawn)

Detailed Description

Real-time audio-video session abstraction layer.

The Session Module provides a unified, cross-platform abstraction for managing real-time audio-video sessions in ascii-chat. It encapsulates:

  • Server-side hosting with multi-client support and media mixing
  • Client-side participation with encrypted streams and state coordination
  • Shared infrastructure for media capture, terminal rendering, and audio handling
  • Consensus protocol for distributed state management and host election
  • Interactive controls via keyboard input and configuration

Module Organization

The session module is organized into several layers:

High-Level APIs

Shared Infrastructure

State Management

Architecture Overview

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Session Module โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ”‚
โ”‚ Server-Side โ† Consensus Protocol โ†’ Client-Side โ”‚
โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚
โ”‚ session_host_t Ring Consensus session_participant_t
โ”‚ - Listen/Accept State Election - Connect
โ”‚ - Client Registry Coordination - Stream
โ”‚ - Media Mixing Distributed - Receive
โ”‚ - Broadcasting State Mgmt - Disconnect
โ”‚ โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ Shared Session Infrastructure โ”‚ โ”‚
โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
โ”‚ โ”‚ Capture (webcam/file/URL) โ”‚ โ”‚
โ”‚ โ”‚ Display (terminal/TTY) โ”‚ โ”‚
โ”‚ โ”‚ Render Loop (unified) โ”‚ โ”‚
โ”‚ โ”‚ Audio (capture/playback) โ”‚ โ”‚
โ”‚ โ”‚ Keyboard (interactive) โ”‚ โ”‚
โ”‚ โ”‚ Settings (RCU-based) โ”‚ โ”‚
โ”‚ โ”‚ Log Buffer (UI support) โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
struct session_participant session_participant_t
Client session state spanning connection, transport, media workers, and callbacks.
struct session_host session_host_t
Opaque session host handle.

Session State Machines

Each major component in the session module has a well-defined state machine:

Host State Machine

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ SESSION HOST LIFECYCLE โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ”‚
โ”‚ session_host_create() โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ CREATED โ”‚ - Allocated but not listening โ”‚
โ”‚ โ”‚ โ”‚ - No network socket yet โ”‚
โ”‚ โ”‚ โ”‚ - Config applied, ready for start โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ session_host_start() โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ LISTENING โ”‚ - Socket listening on configured port โ”‚
โ”‚ โ”‚ โ”‚ - Accepting client connections โ”‚
โ”‚ โ”‚ โ”‚ - Client registry initialized โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ†• (accepts clients) โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ HOSTING โ”‚ - One or more clients connected โ”‚
โ”‚ โ”‚ CLIENTS โ”‚ - Callbacks fired on client events โ”‚
โ”‚ โ”‚ โ”‚ - Render thread can be started โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ session_host_stop() โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ STOPPED โ”‚ - Socket closed, no new connections โ”‚
โ”‚ โ”‚ โ”‚ - Existing clients still drain buffers โ”‚
โ”‚ โ”‚ โ”‚ - Ready for destroy โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ session_host_destroy() โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ DESTROYED โ”‚ - All resources freed โ”‚
โ”‚ โ”‚ โ”‚ - Handle invalid โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
asciichat_error_t session_host_destroy(session_host_t *host)
Destroy session host and free resources.
Definition host.c:289
asciichat_error_t session_host_stop(session_host_t *host)
Stop accepting connections and disconnect all clients.
Definition host.c:1076
asciichat_error_t session_host_start(session_host_t *host)
Start accepting client connections.
Definition host.c:1037
session_host_t * session_host_create(const session_host_config_t *config)
Create a new session host.
Definition host.c:234
bool initialized
Definition mmap.c:41

Participant State Machine

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ SESSION PARTICIPANT LIFECYCLE โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ”‚
โ”‚ session_participant_create() โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ CREATED โ”‚ - Allocated but not connected โ”‚
โ”‚ โ”‚ โ”‚ - Config applied, no network socket โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ session_participant_connect() โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v (handshake) โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ CONNECTING โ”‚ - TCP connection established โ”‚
โ”‚ โ”‚ โ”‚ - Crypto handshake in progress โ”‚
โ”‚ โ”‚ โ”‚ - Waiting for server acknowledgment โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ CONNECTED โ”‚ - Encrypted connection established โ”‚
โ”‚ โ”‚ โ”‚ - Received unique client ID โ”‚
โ”‚ โ”‚ โ”‚ - on_connected callback fired โ”‚
โ”‚ โ”‚ โ”‚ - Ready for video/audio streams โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ โ”œโ”€ session_participant_start_video() โ”‚
โ”‚ โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ v โ”‚
โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ โ”‚ VIDEO STREAMING โ”‚ - Sending local frames โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ - on_frame_received fired โ”‚
โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ โ”œโ”€ session_participant_start_audio() โ”‚
โ”‚ โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ v โ”‚
โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ โ”‚ AUDIO STREAMING โ”‚ - Sending audio samples โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ - on_audio_received fired โ”‚
โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ DISCONNECTEDโ”‚ - Connection closed โ”‚
โ”‚ โ”‚ โ”‚ - Streams stopped, socket closed โ”‚
โ”‚ โ”‚ โ”‚ - on_disconnected callback fired โ”‚
โ”‚ โ”‚ โ”‚ - Ready for destroy โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ session_participant_destroy() โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ DESTROYED โ”‚ - All resources freed โ”‚
โ”‚ โ”‚ โ”‚ - Handle invalid โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
asciichat_error_t session_participant_connect(session_participant_t *p)
Connect to session server.
asciichat_error_t session_participant_start_video(session_participant_t *p)
Start video capture and streaming.
void session_participant_destroy(session_participant_t *p)
Destroy session participant and free resources.
session_participant_t * session_participant_create(const session_participant_config_t *config)
Server address, security, media, transport, and callback settings for a participant.
void session_participant_disconnect(session_participant_t *p)
Disconnect from session server.
asciichat_error_t session_participant_start_audio(session_participant_t *p)
Start audio capture and streaming.

Capture Context State Machine

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ CAPTURE CONTEXT LIFECYCLE โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ”‚
โ”‚ session_capture_create() โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ IDLE โ”‚ - Media source initialized โ”‚
โ”‚ โ”‚ โ”‚ - FFmpeg/webcam opened โ”‚
โ”‚ โ”‚ โ”‚ - First frame potentially buffered โ”‚
โ”‚ โ”‚ โ”‚ - FPS tracking active โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ†• (pause/resume) โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ PLAYING โ”‚ - Frames being read and delivered โ”‚
โ”‚ โ”‚ โ”‚ - FPS regulation active โ”‚
โ”‚ โ”‚ โ”‚ - seek_relative() affects playback โ”‚
โ”‚ โ”‚ โ”‚ - Video loop on file sources โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ†• (pause) โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ PAUSED โ”‚ - Same frame returned repeatedly โ”‚
โ”‚ โ”‚ โ”‚ - seek_relative() queued for resume โ”‚
โ”‚ โ”‚ โ”‚ - FPS regulation continues โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ session_capture_destroy() โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ v โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ DESTROYED โ”‚ - Media source closed โ”‚
โ”‚ โ”‚ โ”‚ - FFmpeg subprocess terminated โ”‚
โ”‚ โ”‚ โ”‚ - Handle invalid โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
audio_visualization_source_t source
Definition fft.c:24
session_capture_ctx_t * session_capture_create(const session_capture_config_t *config)
Legacy function - creates either mirror or network capture based on options.
void session_capture_destroy(session_capture_ctx_t *ctx)
Destroy session capture context and free resources.

Thread Coordination

The session module uses thread-safe components and clear ownership patterns:

Thread Safety by Component

Fully Thread-Safe (Can be accessed from multiple threads):

Thread-Confined (Single-threaded access only):

Multi-Threaded (Clear thread ownership):

Render Loop Threading Pattern

Main Thread Render Thread
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
โ”œโ”€ Initialize capture (main thread)
โ”œโ”€ Initialize display (main thread)
โ”œโ”€ Initialize audio (main thread)
โ”‚
โ”œโ”€ Start render thread โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ โ”‚
โ”‚ v
โ”‚ while (!exit):
โ”‚ read frame (capture)
โ”‚ process frame
โ”‚ write display
โ”‚ handle keyboard โ† lock-free read
โ”‚ update settings โ† RCU read
โ”‚ sleep for FPS
โ”‚
โ”œโ”€ Wait for render thread โ†โ”€โ”˜
โ”‚
โ””โ”€ Cleanup
โ”œโ”€ Destroy capture
โ”œโ”€ Destroy display
โ”œโ”€ Cleanup audio
asciichat_error_t session_client_like_run(const session_client_like_config_t *config)
asciichat_error_t session_render_loop(session_capture_ctx_t *capture, session_display_ctx_t *display, session_should_exit_fn should_exit, session_capture_fn capture_cb, session_sleep_for_frame_fn sleep_cb, session_keyboard_handler_fn keyboard_handler, void *user_data)
Unified render loop for all display modes.
int main(int argc, char *argv[])
Definition main.c:514

Synchronization Primitives

RCU (Read-Copy-Update) for Settings:

  • Multiple readers (render thread) never block
  • Writers (keyboard handler) briefly lock for atomic swap
  • Use case: pause state, volume, color mode changes

Circular Buffer for Logs:

  • Lock-free append for log entries
  • Atomic read for recent entries
  • Multiple readers, single writer pattern

Mutex-Based Synchronization (if needed):

Capture Integration

The capture abstraction integrates seamlessly with the render loop and state machine:

Capture Sources and Behavior

Webcam Source (Direct Platform Camera):

  • Opens platform camera device (e.g., /dev/video0 on Linux)
  • Reads frames at native device frame rate
  • session_capture_sleep_for_fps() throttles to target FPS
  • No seek support (live stream)

File Source (FFmpeg):

  • Spawns FFmpeg subprocess to decode video
  • Reads frames via stdout pipe at variable rate
  • Seeks supported: session_capture_seek_relative()
  • Loop support: restarts FFmpeg when end reached

URL Source (FFmpeg with yt-dlp resolution):

  • Media resolver (lib/media/source.c):
    1. Direct detection: .mp4, .m3u8 โ†’ FFmpeg directly (fastest)
    2. yt-dlp extraction: YouTube, TikTok, etc. โ†’ extract stream URL
    3. Fallback: try FFmpeg as last resort
  • Result: streaming URL passed to FFmpeg subprocess
  • Seek support varies by protocol (HLS/DASH may not support)

Test Pattern (Procedural):

  • Generates frames algorithmically
  • Useful for testing without camera/media files
  • Precise frame rate control

FPS Detection and Adaptation

The client_like framework probes source FPS:

// During initialization:
session_capture_probe_fps(capture, 1.0) // Read for 1 second
// Result: detected_fps (e.g., 30, 60, etc.)
// Render loop then uses:
session_capture_sleep_for_fps(capture) // Adaptive sleep
void session_capture_sleep_for_fps(session_capture_ctx_t *ctx)
Sleep to maintain target frame rate.

Adaptive Sleep Strategy:

  • Tracks actual frame delivery times
  • Compensates for processing delays
  • Prevents frame drops or excessive buffering
  • Critical for smooth playback

Pause/Resume Behavior

Pause is coordinated via session_settings_*:

// Keyboard handler (any thread):
session_settings_set_pause_state(true);
// Render loop (single-threaded):
while (!exit) {
bool paused = session_settings_get_pause_state(); // Lock-free read
image_t *frame = session_capture_read_frame(ctx); // Pause state affects?
// Paused: same frame returned repeatedly
// Playing: next frame from source
}
image_t * session_capture_read_frame(session_capture_ctx_t *ctx)
Read the next video frame from the capture source.
SIMD-friendly storage for one RGB pixel. r, g, and b hold the color channels and padding keeps the la...

Resource Lifecycle

Complete lifecycle from creation to destruction:

Initialization Sequence (session_client_like_run)

1. Keepawake System
โ””โ”€ Prevent OS sleep while running
2. Terminal Output Check
โ””โ”€ Detect if stdout is piped (affects splash screen)
3. Media Source Setup
โ”œโ”€ session_capture_create()
โ”œโ”€ Probe source FPS
โ””โ”€ Initialize frame buffer
4. Audio System Setup
โ”œโ”€ Platform audio context (PortAudio, JACK, etc.)
โ”œโ”€ Allocate audio buffers
โ””โ”€ Start capture thread (if needed)
5. Display Context Creation
โ”œโ”€ Detect terminal capabilities (color, resolution)
โ”œโ”€ Initialize color palette
โ””โ”€ Set up rendering state
6. Splash Screen
โ”œโ”€ Animated ASCII art display
โ”œโ”€ Shows log capture buffer
โ””โ”€ User can dismiss with keypress
7. Mode-Specific Initialization
โ””โ”€ Passed to run_fn callback (mode-dependent)
8. Main Loop Execution
โ””โ”€ session_render_loop() or custom mode loop
if(!queries||!out||index >=DATABASE_QUERY_COUNT) return false

Cleanup Sequence (Error Recovery and Normal Exit)

1. Exit Render Loop
โ””โ”€ Graceful shutdown signal set
2. Render Thread Join
โ””โ”€ Wait for render thread to finish current frame
3. Keyboard Handler Cleanup
โ”œโ”€ Clear any queued input
โ””โ”€ Disable async input if needed
4. Audio System Shutdown
โ”œโ”€ Stop capture thread
โ”œโ”€ Drain remaining samples
โ””โ”€ CRITICAL: Stop BEFORE display cleanup (PortAudio constraint)
5. Capture Context Cleanup
โ”œโ”€ Close media source
โ”œโ”€ Terminate FFmpeg if running
โ””โ”€ Free frame buffers
6. Display Context Cleanup
โ”œโ”€ Restore terminal state
โ”œโ”€ Clear any partial renders
โ””โ”€ Re-enable cursor if needed
7. Keepawake Release
โ””โ”€ Re-enable system sleep
8. Memory Reports
โ””โ”€ Print leak detection (debug builds)

Critical Ordering Notes:

  • Audio MUST be stopped before display (PortAudio cleans up terminal)
  • Display cleanup must restore full terminal state before exit
  • Render thread must be joined before freeing shared buffers

Memory Ownership

Clear ownership prevents double-frees and memory leaks:

session_client_like_run owns and manages:

Mode callbacks receive:

  • Initialized, ready-to-use contexts
  • MUST NOT attempt to free or destroy contexts
  • Can read and modify state through provided APIs

Framebuffer Policy:

  • Capture context owns frame buffers returned by session_capture_read_frame()
  • Caller receives pointer, must not free
  • Capture destruction releases all frame buffers

Error Recovery

If error occurs during execution:

capture_open fails โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
audio_start fails โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ”€โ†’ Skip mode callback
display_create fails โ”€โ”€โ”€โ”€โ”€โ”คโ”€โ†’ Run cleanup sequence
render_loop fails โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”€โ†’ Exit with error
Result: all allocated resources freed, terminal restored

Key Concepts

Media Capture

The capture abstraction (session_capture_ctx_t) unifies all media sources:

  • Webcam: Direct camera input via platform abstraction
  • Files: Local video files (MP4, MKV, etc.) via FFmpeg
  • URLs: Streaming URLs (HTTP, RTSP, DASH, HLS) via FFmpeg
  • YouTube/TikTok: Via yt-dlp extraction, then FFmpeg playback
  • Test Patterns: Procedurally generated for testing

All sources provide a unified interface:

Terminal Display

The display abstraction (session_display_ctx_t) manages terminal rendering:

  • TTY Detection: Check if output is a terminal
  • Capability Detection: Detect color support (mono/16/256/truecolor)
  • Palette Selection: Choose luminance mapping for ASCII conversion
  • Frame Rendering: Convert and output to terminal
  • Color Modes: Support for ANSI, 256-color, and true color

Render Loop

The unified render loop (session_render_loop) abstracts two patterns:

Synchronous Mode (with capture context):

  • Capture drives the loop
  • Frame rate determined by media source
  • Automatic FPS limiting via adaptive sleep
  • Used by: mirror mode, discovery participant mode

Event-Driven Mode (with custom callbacks):

  • Protocol/network drives the loop
  • Custom frame and sleep callbacks
  • Integration with network event loops
  • Used by: client mode (network receives frames)

Audio Handling

Audio operations are decoupled from media source:

  • Capture: Get samples from default audio input (microphone)
  • Playback: Send samples to default audio output (speakers)
  • Audio Context: Platform-specific state (PortAudio, JACK optional)
  • Format: Float32 mono, normalized to [-1.0, 1.0]

Keyboard Input

Interactive controls via keyboard (session_handle_keyboard_input):

  • ? - Toggle help screen
  • SPACE - Play/pause
  • โ† / โ†’ - Seek ยฑ30 seconds
  • โ†‘ / โ†“ - Volume ยฑ10%
  • m - Mute toggle
  • c - Cycle color modes
  • f - Flip webcam (mirror)

Thread-safe: Can be called from any thread (including render threads)

State Management

Session settings (session_settings_*) use RCU (Read-Copy-Update) for lock-free reads and atomic writes:

  • session_settings_get_volume() - Read current volume (lock-free)
  • session_settings_set_color_mode() - Update color mode (brief lock)
  • session_settings_get_pause_state() - Check pause state (lock-free)

Manages:

  • Color mode (mono/16/256/true color)
  • Pause state (playing/paused)
  • Volume level (0.0 to 1.0)
  • Flip state (for webcam mirroring)
  • Help screen visibility

Consensus Protocol

The consensus module (session_consensus_t) implements a distributed ring consensus protocol for:

  • Host Election: Elect the best-suited participant as session host
  • State Coordination: Synchronize pause/play state across participants
  • Metrics Collection: Gather network quality metrics from all participants
  • Topology Management: Maintain ring topology for state propagation

Used primarily in Discovery Mode for P2P scenarios without a central server.

Usage Patterns

Server Mode

// Configure server
.port = 27224,
.max_clients = 32,
.callbacks = {
.on_client_join = handle_join,
.on_frame_received = handle_frame,
.on_audio_received = handle_audio,
},
.user_data = app_context
};
// Create and start
// Render thread (mixes video/audio)
// Cleanup
void session_host_stop_render(session_host_t *host)
Stop media rendering thread.
Definition host.c:1669
asciichat_error_t session_host_start_render(session_host_t *host)
Start media rendering thread (video mixing and audio distribution).
Definition host.c:1632
Host-side event hooks for joins, departures, media, and errors.
Definition host.h:159

Client Mode

// Configure participant
.address = "server.example.com",
.port = 27224,
.callbacks = {
.on_connected = handle_connected,
.on_frame_received = render_frame,
.on_audio_received = play_audio,
},
.user_data = app_context
};
// Create and connect
// Start streams
// Cleanup
Client-side event hooks for connection, media, settings, and errors.

Mirror Mode (via client_like framework)

// Configure unified framework
.media_source = { .type = MEDIA_SOURCE_WEBCAM },
.run_fn = mirror_main_loop,
.run_user_data = app_context,
// ... audio, display, keyboard config ...
};
// Run (all setup and teardown handled)
// Automatic cleanup
struct session_client_like_config session_client_like_config_t
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ MEDIA_SOURCE_WEBCAM
Hardware webcam device.
Definition source.h:98

Error Handling

All functions return asciichat_error_t. On error:

if (result != ASCIICHAT_OK) {
if (HAS_ERRNO(&ctx)) {
log_error("Failed: %s", ctx.context_message);
}
return result;
}
#define HAS_ERRNO(context)
@ ASCIICHAT_OK
Definition error_codes.h:46
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634

Thread Safety

  • session_host_t: Fully thread-safe for all operations
    • Callbacks may be invoked from any thread
    • Safe to call from render threads
  • session_participant_t: Thread-safe for stream control
    • Async send operations
    • Receive callbacks from dedicated thread
  • session_capture_ctx_t: Single-threaded design
    • Safe to call from single render thread
    • Intended for synchronous media reading
  • session_display_ctx_t: Single-threaded design
    • Safe to call from single render thread
    • Terminal operations not atomic across threads
  • **session_settings_***: Fully thread-safe (RCU-based)
    • Lock-free reads
    • Brief lock for writes
    • Safe from any thread
  • session_handle_keyboard_input(): Thread-safe
    • Can be called from any thread
    • Safe to call concurrently

Memory Management

All allocations are tracked via SAFE_MALLOC/CALLOC/FREE:

// Automatic tracking
uint8_t *buffer = SAFE_MALLOC(1024, uint8_t *);
// Memory report printed at program exit (debug builds)
SAFE_FREE(buffer);
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
unsigned char uint8_t
Definition common.h:60

Encryption and Authentication

All connections are encrypted by default using:

  • X25519: Key exchange
  • XSalsa20-Poly1305: AEAD symmetric encryption
  • Ed25519: Signature verification (SSH key auth)

Supported authentication modes:

  • Ephemeral: Anonymous encrypted connection
  • Password: Both sides prove knowledge of password
  • SSH Key: Server authenticates with Ed25519 key

Configuration:

config.encryption_enabled = true; // Default
config.password = "secret"; // Password auth
config.key_path = "~/.ssh/id_ed25519"; // SSH key auth
bool encryption_enabled
Enable encryption (default: true).
Definition host.h:173
const char * key_path
Path to server identity key.
Definition host.h:179
const char * password
Password for client authentication (optional).
Definition host.h:182

Files Reference

host.h - Server session management participant.h - Client session participation client_like.h - Unified client-like mode framework capture.h - Media source abstraction display.h - Terminal rendering abstraction render.h - Unified render loop audio.h - Audio capture and playback keyboard_handler.h - Interactive keyboard controls session_log_buffer.h - Log capture for UI settings.h - Thread-safe session settings consensus.h - Distributed consensus protocol consensus_integration.h - Consensus integration helpers

See Also

  • Session README: README.md (comprehensive overview)
  • Cryptography: lib/crypto/ (encryption implementation)
  • Network Protocol: lib/network/ (ACIP packet protocol)
  • Media: lib/media/ (FFmpeg/yt-dlp integration)
  • Platform: lib/platform/ (cross-platform abstractions)

This header provides a thin wrapper around the lib/audio/ system for session-level audio coordination. It handles the differences between host (mixing) and participant (simple capture/playback) modes.

CORE FEATURES:

  • Unified API for both host and participant audio handling
  • Host-only audio mixing for multi-participant sessions
  • Source management for tracking participant audio streams
  • Clean startup/shutdown coordination

USAGE:

// Create audio context for a participant
// Start capture and playback
// Cleanup
struct session_audio_ctx session_audio_ctx_t
Opaque session audio context handle.
asciichat_error_t session_audio_start_capture(session_audio_ctx_t *ctx)
Start audio capture (microphone input).
void session_audio_destroy(session_audio_ctx_t *ctx)
Destroy session audio context and free resources.
asciichat_error_t session_audio_start_playback(session_audio_ctx_t *ctx)
Start audio playback (speaker output).
session_audio_ctx_t * session_audio_create(bool is_host)
Create a new session audio context.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

This header provides a unified interface for media capture that abstracts over webcam, file, stdin, and test pattern sources. It is designed to be reusable across different modes (client, mirror, and discovery mode).

CORE FEATURES:

  • Unified API for all media source types (webcam, file, stdin, test)
  • Configurable frame rate with adaptive sleep
  • Optional frame resizing for network transmission
  • FPS tracking and reporting
  • Loop support for file sources

USAGE:

// Create capture context for webcam at 60 FPS
.path = "0",
.target_fps = 60,
.loop = false,
.resize_for_network = false
};
// Read and process frames
while (!done) {
if (frame) {
// Process frame...
}
}
// Cleanup
struct session_capture_ctx session_capture_ctx_t
session capture config keeps the values that move through the subsystem: type, path,...
Media-source and loader settings used to create a session capture context.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

This header provides a unified interface for terminal display that abstracts TTY detection, palette initialization, and frame rendering. It is designed to be reusable across different modes (client, mirror, and discovery mode).

CORE FEATURES:

  • TTY detection and management
  • Terminal capability detection (color, UTF-8)
  • Palette initialization and luminance mapping
  • Frame rendering with RLE expansion support
  • Snapshot mode support for single-frame capture

USAGE:

// Create display context with color support
.snapshot_mode = false,
.palette_type = PALETTE_STANDARD,
.custom_palette = NULL,
.color_mode = TERM_COLOR_AUTO
};
// Render frames
const char *frame_data = ...; // ASCII frame from server
session_display_render_frame(ctx, frame_data, false);
// Cleanup
@ PALETTE_STANDARD
Standard ASCII palette: " ...',;:clodxkO0KXNWM".
Definition palette.h:94
void session_display_destroy(session_display_ctx_t *ctx)
session_display_ctx_t * session_display_create(const session_display_config_t *config)
Create a new session display context.
struct session_display_ctx session_display_ctx_t
session display config is intentionally a small, concrete record. Fields such as snapshot_mode,...
void session_display_render_frame(session_display_ctx_t *ctx, const char *frame_data)
Render an ASCII frame to the terminal.
Terminal-rendering and snapshot settings used to create a display context.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

This header provides the server-side session hosting abstraction, encapsulating client management, connection acceptance, and event handling for session hosts.

CORE FEATURES:

  • Server lifecycle management (start, stop)
  • Client connection handling (add, remove, lookup)
  • Callback-based event notification
  • Encryption and authentication support

USAGE:

// Define callbacks
void on_client_join(session_host_t *h, uint32_t id, void *data) {
printf("Client %u joined\n", id);
}
void on_frame(session_host_t *h, uint32_t id, const image_t *frame, void *data) {
// Process video frame from client
}
// Create and start host
.port = 27224,
.max_clients = 32,
.callbacks = { .on_client_join = on_client_join, .on_frame_received = on_frame }
};
// Run until stopped...
// Cleanup
unsigned int uint32_t
Definition common.h:62
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

Provides keyboard input handling for session-level controls including:

  • Media playback control (play/pause, seek)
  • Audio control (volume, mute)
  • Display control (color mode, webcam flip)
  • Help screen toggle
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

This header provides the client-side session participation abstraction, encapsulating connection management, media streaming, and event handling for session participants.

CORE FEATURES:

  • Connection lifecycle management (connect, disconnect, reconnect)
  • Video and audio stream control
  • Callback-based event notification
  • Encryption and authentication support

USAGE:

// Define callbacks
void on_connected(session_participant_t *p, uint32_t id, void *data) {
printf("Connected with ID %u\n", id);
}
void on_frame(session_participant_t *p, const char *frame, void *data) {
// Render ASCII frame
}
// Create and connect
.address = "127.0.0.1",
.port = 27224,
.enable_video = true,
.callbacks = { .on_connected = on_connected, .on_frame_received = on_frame }
};
// Start streaming
return;
}
// Cleanup
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

Provides an interactive keyboard help overlay that displays:

  • Available keyboard shortcuts
  • Current program state (volume, color mode, webcam flip, audio, mute status)

The keyboard help is toggled with '?' key and suppresses frame rendering while network reception continues in the background.

Display State:

  • Horizontally and vertically centered in terminal
  • Box-drawing border with UTF-8 fallback to ASCII
  • Real-time updates of option values (no network latency)

Threading:

  • Keyboard help state: Shared atomic interactive-screen selection
  • Terminal writes: Serialized via session_display_write_raw()
  • Option reads: Lock-free via RCU (GET_OPTION macro)
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
February 2026

Provides a blocking prompt screen that shows when a new version of ascii-chat is available. Displayed after the splash screen ends, before video rendering begins. The user can choose to exit and update (Y/Enter) or continue (N/Esc).

Thread-safe: update_banner_set_result() can be called from the background update check thread while the splash is still running.

Provides a rolling-window FPS counter that measures the actual throughput of frame output operations (platform_write_all). Maintains a circular buffer of timestamps to compute stable FPS readings.

USAGE:

// After each frame write to terminal
// Get current FPS for display
float current_fps = fps_counter_get(fps);
float fps_counter_get(fps_counter_t *counter)
Get the current FPS value.
void fps_counter_tick(fps_counter_t *counter)
Record a frame timestamp.
Definition fps_counter.c:78
void fps_counter_destroy(fps_counter_t *counter)
Destroy FPS counter and free resources.
Definition fps_counter.c:68
struct fps_counter_s fps_counter_t
Opaque FPS counter handle.
Definition fps_counter.h:40
fps_counter_t * fps_counter_create(void)
Create a new FPS counter.
Definition fps_counter.c:50
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
February 2026

Typedef Documentation

◆ acip_transport_t [1/2]

#include <src/common/session/host.h>

Forward declaration of ACIP transport.

Forward declaration of transport implementation.

Opaque transport interface for send/receive operations. Used for WebRTC DataChannels, WebSockets, and other transports.

See also
acip_transport_t in network/acip/transport.h

Opaque handle to transport-specific state. Each transport implementation defines its own state structure.

Definition at line 506 of file host.h.

◆ acip_transport_t [2/2]

#include <src/common/session/participant.h>

Forward declaration of ACIP transport.

Opaque transport interface for send/receive operations. Used for WebRTC DataChannels, WebSockets, and other transports.

See also
acip_transport_t in network/acip/transport.h

Definition at line 377 of file participant.h.

◆ fps_counter_t

typedef struct fps_counter_s fps_counter_t

#include <include/ascii-chat/ui/fps_counter.h>

Opaque FPS counter handle.

Maintains a rolling window of frame timestamps for FPS calculation.

Definition at line 40 of file fps_counter.h.

◆ session_audio_ctx_t

#include <src/common/session/audio.h>

Opaque session audio context handle.

Manages audio capture, playback, and optional mixing state. Created via session_audio_create(), destroyed via session_audio_destroy().

Definition at line 56 of file src/common/session/audio.h.

◆ session_capture_ctx_t

#include <src/common/session/capture.h>

session capture config keeps the values that move through the subsystem: type, path, target_fps, loop. Some members are pointers into caller- or subsystem-owned memory, so lifetime follows the API that populated them rather than the struct copy itself.

Opaque session capture context handle

Manages media source, FPS tracking, and adaptive sleep state. Created via session_capture_create(), destroyed via session_capture_destroy().

Definition at line 134 of file common/session/capture.h.

◆ session_capture_fn

typedef image_t *(* session_capture_fn) (void *user_data)

#include <src/common/session/render.h>

Capture callback for event-driven frame source.

Called to obtain the next frame for rendering. Can return NULL if no frame is currently available.

Used when capture context is NULL (event-driven mode). Not called if capture context is provided (synchronous mode).

Parameters
user_dataOpaque pointer provided by caller
Returns
Pointer to frame (caller must not free), or NULL if unavailable

Definition at line 59 of file src/common/session/render.h.

◆ session_capture_should_exit_fn

typedef bool(* session_capture_should_exit_fn) (void *user_data)

#include <src/common/session/capture.h>

Callback type to check if initialization should be cancelled.

Called periodically during initialization to allow graceful cancellation. Should return true if initialization should stop immediately.

Parameters
user_dataOpaque pointer provided by caller
Returns
true to cancel initialization, false to continue

Definition at line 71 of file common/session/capture.h.

◆ session_display_ctx_t [1/3]

◆ session_display_ctx_t [2/3]

#include <src/client/display.h>

session display config is intentionally a small, concrete record. Fields such as snapshot_mode, palette_type, custom_palette, color_mode give callers the identity and current value they need. It is normally borrowed from, or owned by, the surrounding subsystemโ€”not independently managed.

Opaque session display context handle

Manages TTY state, terminal capabilities, palette, and rendering state. Created via session_display_create(), destroyed via session_display_destroy().

Definition at line 29 of file src/client/display.h.

◆ session_display_ctx_t [3/3]

#include <src/common/session/display.c>

Runtime terminal state and reusable workspaces for ASCII rendering.

Runtime terminal state and reusable workspaces for ASCII rendering.

It owns TTY capability data, palette lookup tables, stabilizers, output and resize buffers, render-file timing, and the flags that stop output after the consumer disappears. Audio context pointers remain borrowed from the caller.

◆ session_display_should_exit_fn

typedef bool(* session_display_should_exit_fn) (void *user_data)

#include <src/common/session/display.h>

Callback type to check if initialization should be cancelled.

Called periodically during initialization to allow graceful cancellation. Should return true if initialization should stop immediately.

Parameters
user_dataOpaque pointer provided by caller
Returns
true to cancel initialization, false to continue

Definition at line 66 of file src/common/session/display.h.

◆ session_host_t

typedef struct session_host session_host_t

#include <src/common/session/host.c>

Opaque session host handle.

Manages server state, client connections, and event callbacks. Created via session_host_create(), destroyed via session_host_destroy().

◆ session_keyboard_handler_fn

typedef void(* session_keyboard_handler_fn) (session_capture_ctx_t *capture, int key, void *user_data)

#include <src/common/session/render.h>

Keyboard input handler callback type.

Called when keyboard input is detected during render loop iteration. Allows applications to respond to interactive keyboard controls.

Parameters
captureCapture context (may be NULL for client mode)
keyKeyboard key code from keyboard_read_nonblocking()
user_dataOpaque pointer provided by caller

Definition at line 87 of file src/common/session/render.h.

◆ session_participant_t

#include <src/common/session/participant.c>

Client session state spanning connection, transport, media workers, and callbacks.

Opaque session participant handle.

Client session state spanning connection, transport, media workers, and callbacks.

The participant owns its copied address/password settings and worker handles, while the selected transport and network connection follow their explicit ownership fields. Audio/video lifecycle state prevents stale workers from using a connection during shutdown.

Contains connection state, media stream state, and callback configuration.

Manages connection state, media streams, and event callbacks. Created via session_participant_create(), destroyed via session_participant_destroy().

◆ session_should_exit_fn

typedef bool(* session_should_exit_fn) (void *user_data)

#include <src/common/session/render.h>

Exit condition callback type.

Called each iteration to check if render loop should terminate. Return true to exit, false to continue.

Parameters
user_dataOpaque pointer provided by caller
Returns
true if loop should exit, false to continue

Definition at line 43 of file src/common/session/render.h.

◆ session_sleep_for_frame_fn

typedef void(* session_sleep_for_frame_fn) (void *user_data)

#include <src/common/session/render.h>

Sleep callback for custom frame timing.

Called to sleep/wait until the next frame is ready. Allows custom timing logic.

Used when capture context is NULL (event-driven mode). Not called if capture context is provided (synchronous mode).

Parameters
user_dataOpaque pointer provided by caller

Definition at line 73 of file src/common/session/render.h.

Function Documentation

◆ fps_counter_create()

fps_counter_t * fps_counter_create ( void )

#include <include/ascii-chat/ui/fps_counter.h>

Create a new FPS counter.

Returns
Pointer to counter, or NULL on failure

Creates and initializes a rolling-window FPS counter with a buffer for 30 frame timestamps.

Note
Call fps_counter_destroy() to free resources when done.

Definition at line 50 of file fps_counter.c.

50 {
51 fps_counter_t *counter = SAFE_CALLOC(1, sizeof(fps_counter_t), fps_counter_t *);
52 if (!counter) {
53 return NULL;
54 }
55
56 counter->last_fps = -1;
57 counter->effective_fps = 60;
58 counter->report_start = time_get_ns();
59
60 // Initialize circular buffer pointers
61 counter->head = 0;
62 counter->count = 0;
63 memset(counter->frame_times, 0, sizeof(counter->frame_times));
64
65 return counter;
66}
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
uint64_t report_start
Definition fps_counter.c:38
int count
Number of valid entries in buffer (0 to FPS_WINDOW_SIZE).
Definition fps_counter.c:43
int head
Current write position (0 to FPS_WINDOW_SIZE-1).
Definition fps_counter.c:42
uint64_t frame_times[30]
Circular buffer of timestamps (ns).
Definition fps_counter.c:41
unsigned int effective_fps
Definition fps_counter.c:30

References fps_counter_s::count, fps_counter_s::effective_fps, fps_counter_s::frame_times, fps_counter_s::head, fps_counter_s::last_fps, fps_counter_s::report_start, and SAFE_CALLOC.

◆ fps_counter_destroy()

void fps_counter_destroy ( fps_counter_t * counter)

#include <include/ascii-chat/ui/fps_counter.h>

Destroy FPS counter and free resources.

Parameters
counterCounter to destroy (can be NULL)

Definition at line 68 of file fps_counter.c.

68 {
69 if (counter) {
70 SAFE_FREE(counter);
71 }
72}

References SAFE_FREE.

◆ fps_counter_frame_begin()

void fps_counter_frame_begin ( fps_counter_t * counter,
bool measure )

#include <include/ascii-chat/ui/fps_counter.h>

Definition at line 161 of file fps_counter.c.

161 {
162 if (!counter)
163 return;
164 counter->current_frame_id = 0;
165 counter->measuring = measure;
166 counter->written = counter->failed = false;
167 counter->frame_write_ns = 0;
168}
uint64_t frame_write_ns
Definition fps_counter.c:37
uint64_t current_frame_id
Definition fps_counter.c:35

References fps_counter_s::current_frame_id, fps_counter_s::failed, fps_counter_s::frame_write_ns, fps_counter_s::measuring, and fps_counter_s::written.

Referenced by fps_counter_frame_begin_id().

◆ fps_counter_frame_end()

void fps_counter_frame_end ( fps_counter_t * counter,
uint64_t now )

#include <include/ascii-chat/ui/fps_counter.h>

Finish at the supplied monotonic timestamp; failed or empty presentations do not count.

Definition at line 191 of file fps_counter.c.

191 {
192 if (!counter)
193 return;
194 if (counter->measuring && counter->written && !counter->failed) {
195 fps_counter_tick_at(counter, now);
196 if (counter->current_frame_id)
197 counter->completed_frame_id = counter->current_frame_id;
198 counter->report_writes += counter->frame_write_ns;
199 counter->report_frames++;
200 }
201 counter->measuring = false;
202}
void fps_counter_tick_at(fps_counter_t *counter, uint64_t now)
Definition fps_counter.c:89
uint64_t completed_frame_id
Definition fps_counter.c:35
uint64_t report_writes
Definition fps_counter.c:39
uint64_t report_frames
Definition fps_counter.c:40

References fps_counter_s::completed_frame_id, fps_counter_s::current_frame_id, fps_counter_s::failed, fps_counter_tick_at(), fps_counter_s::frame_write_ns, fps_counter_s::measuring, fps_counter_s::report_frames, fps_counter_s::report_writes, and fps_counter_s::written.

◆ fps_counter_get()

float fps_counter_get ( fps_counter_t * counter)

#include <include/ascii-chat/ui/fps_counter.h>

Get the current FPS value.

Parameters
counterFPS counter (can be NULL)
Returns
Current FPS as a float, or 0.0 if counter is NULL or insufficient data

Returns a rolling-window average FPS based on the last 30 frames. Stable FPS reading after at least 2 frames have been recorded. Returns zero after two seconds without a completed frame. The next tick starts a fresh window so pauses do not distort resumed playback.

Definition at line 112 of file fps_counter.c.

112 {
113 return fps_counter_get_at(counter, time_get_ns());
114}
float fps_counter_get_at(fps_counter_t *counter, uint64_t now)

References fps_counter_get_at().

◆ fps_counter_get_at()

float fps_counter_get_at ( fps_counter_t * counter,
uint64_t now )

#include <include/ascii-chat/ui/fps_counter.h>

Definition at line 116 of file fps_counter.c.

116 {
117 if (!counter || counter->count < 2) {
118 return 0.0f;
119 }
120
121 // Find indices of oldest and newest timestamps in the buffer
122 int oldest_idx = (counter->head - counter->count + FPS_WINDOW_SIZE) % FPS_WINDOW_SIZE;
123 int newest_idx = (counter->head - 1 + FPS_WINDOW_SIZE) % FPS_WINDOW_SIZE;
124
125 // Get the elapsed time between oldest and newest frames
126 uint64_t oldest_time = counter->frame_times[oldest_idx];
127 uint64_t newest_time = counter->frame_times[newest_idx];
128 if (now < newest_time || now - newest_time >= 2000000000ULL)
129 return 0.0f;
130 // Include time since the last completed frame so stalled output decays immediately.
131 uint64_t elapsed_ns = now - oldest_time;
132
133 // Avoid division by zero
134 if (elapsed_ns == 0) {
135 return 0.0f;
136 }
137
138 // FPS = (number of frames) / elapsed_time
139 // We have (count - 1) frames across the elapsed time
140 // (count - 1) frames means count timestamps, so count-1 intervals
141 return (float)(counter->count - 1) * 1e9f / (float)elapsed_ns;
142}
#define FPS_WINDOW_SIZE
Number of frames to average over.
Definition fps_counter.c:17
unsigned long long uint64_t
Definition common.h:63

References fps_counter_s::count, FPS_WINDOW_SIZE, fps_counter_s::frame_times, and fps_counter_s::head.

Referenced by fps_counter_get(), and fps_counter_render().

◆ fps_counter_render()

void fps_counter_render ( fps_counter_t * counter,
int fd,
int columns,
bool redrawn )

#include <include/ascii-chat/ui/fps_counter.h>

Draw the overlay if visible and changed, and periodically report output timing.

Definition at line 204 of file fps_counter.c.

204 {
205 if (!counter)
206 return;
207 uint64_t now = time_get_ns();
208 float measured = fps_counter_get_at(counter, now);
209 // A UI overlay can repaint for longer than the media counter's inactivity
210 // window. Keep the last visible value during a forced overlay redraw so
211 // opening keyboard help does not make an otherwise-live stream display 0.
212 int value = measured >= 999.0f ? 999 : (int)(measured + 0.5f);
213 if (redrawn && value == 0 && counter->last_fps > 0)
214 value = counter->last_fps;
215 // `FPS:ddd/ddd` is ten columns wide. Start nine columns before the
216 // terminal's rightmost column so the complete overlay stays on one line.
217 if (counter->visible && columns >= 10 && (redrawn || value != counter->last_fps)) {
218 // Restore the cursor so the right-margin overlay cannot wrap the next write.
219 const char *style = terminal_has_dark_background() ? "\033[97;40m" : "\033[30;47m";
220 if (ui_output_printf(fd, "\0337\033[1;%dH%sFPS:%3d/%u\033[0m\0338", columns - 9, style, value,
221 counter->effective_fps) == ASCIICHAT_OK)
222 counter->last_fps = value;
223 }
224 if (now - counter->report_start >= 3 * NS_PER_SEC_INT) {
225 if (counter->report_frames)
226 log_debug("FPS_OUTPUT: frames=%llu elapsed_ms=%.3f write_ms=%.3f", (unsigned long long)counter->report_frames,
227 (double)(now - counter->report_start) / NS_PER_MS_INT, (double)counter->report_writes / NS_PER_MS_INT);
228 counter->report_start = now;
229 counter->report_writes = counter->report_frames = 0;
230 }
231}
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
#define NS_PER_SEC_INT
Definition time.h:169
#define NS_PER_MS_INT
Definition time.h:168
asciichat_error_t ui_output_printf(int fd, const char *format,...)
Definition output.c:26

References ASCIICHAT_OK, fps_counter_s::effective_fps, fps_counter_get_at(), fps_counter_s::last_fps, log_debug, NS_PER_MS_INT, NS_PER_SEC_INT, fps_counter_s::report_frames, fps_counter_s::report_start, fps_counter_s::report_writes, ui_output_printf(), and fps_counter_s::visible.

◆ fps_counter_reset()

void fps_counter_reset ( fps_counter_t * counter)

#include <include/ascii-chat/ui/fps_counter.h>

Clear samples when the displayed screen changes. Single-owner API.

Definition at line 82 of file fps_counter.c.

82 {
83 if (counter) {
84 counter->head = counter->count = 0;
85 memset(counter->frame_times, 0, sizeof(counter->frame_times));
86 }
87}

References fps_counter_s::count, fps_counter_s::frame_times, and fps_counter_s::head.

Referenced by fps_counter_tick_at().

◆ fps_counter_set_effective_fps()

void fps_counter_set_effective_fps ( fps_counter_t * counter,
unsigned int fps )

#include <include/ascii-chat/ui/fps_counter.h>

Set the effective frame-rate target shown beside the measured rate.

Definition at line 154 of file fps_counter.c.

154 {
155 if (counter) {
156 counter->effective_fps = fps > 0 ? fps : 60;
157 counter->last_fps = -1;
158 }
159}

References fps_counter_s::effective_fps, and fps_counter_s::last_fps.

Referenced by ui_controller_set_effective_fps().

◆ fps_counter_set_visible()

bool fps_counter_set_visible ( fps_counter_t * counter,
bool visible )

#include <include/ascii-chat/ui/fps_counter.h>

Presentation hooks. All calls for a counter must run on its owning thread.

Definition at line 144 of file fps_counter.c.

144 {
145 if (!counter)
146 return false;
147 bool changed = counter->visible != visible;
148 counter->visible = visible;
149 if (changed)
150 counter->last_fps = -1;
151 return changed;
152}

References fps_counter_s::last_fps, and fps_counter_s::visible.

◆ fps_counter_tick()

void fps_counter_tick ( fps_counter_t * counter)

#include <include/ascii-chat/ui/fps_counter.h>

Record a frame timestamp.

Parameters
counterFPS counter (must not be NULL)

Call this immediately after a frame write completes to record the output throughput. Uses nanosecond-precision timing.

Definition at line 78 of file fps_counter.c.

78 {
79 fps_counter_tick_at(counter, time_get_ns());
80}

References fps_counter_tick_at().

◆ fps_counter_tick_at()

void fps_counter_tick_at ( fps_counter_t * counter,
uint64_t now )

#include <include/ascii-chat/ui/fps_counter.h>

Record/read using an explicit monotonic timestamp, in nanoseconds.

Definition at line 89 of file fps_counter.c.

89 {
90 if (!counter) {
91 return;
92 }
93
94 if (counter->count) {
95 uint64_t previous = counter->frame_times[(counter->head + FPS_WINDOW_SIZE - 1) % FPS_WINDOW_SIZE];
96 if (now <= previous || now - previous >= 2000000000ULL)
97 fps_counter_reset(counter);
98 }
99
100 // Record current time at head position
101 counter->frame_times[counter->head] = now;
102
103 // Advance head pointer with wraparound
104 counter->head = (counter->head + 1) % FPS_WINDOW_SIZE;
105
106 // Track how many valid entries we have (up to FPS_WINDOW_SIZE)
107 if (counter->count < FPS_WINDOW_SIZE) {
108 counter->count++;
109 }
110}
void fps_counter_reset(fps_counter_t *counter)
Definition fps_counter.c:82

References fps_counter_s::count, fps_counter_reset(), FPS_WINDOW_SIZE, fps_counter_s::frame_times, and fps_counter_s::head.

Referenced by fps_counter_frame_end(), and fps_counter_tick().

◆ fps_counter_write_begin()

void fps_counter_write_begin ( fps_counter_t * counter)

#include <include/ascii-chat/ui/fps_counter.h>

Definition at line 178 of file fps_counter.c.

178 {
179 if (counter && counter->measuring)
180 counter->write_start = time_get_ns();
181}
uint64_t write_start
Definition fps_counter.c:36

References fps_counter_s::measuring, and fps_counter_s::write_start.

Referenced by ui_controller_write().

◆ fps_counter_write_end()

void fps_counter_write_end ( fps_counter_t * counter,
bool complete )

#include <include/ascii-chat/ui/fps_counter.h>

Definition at line 183 of file fps_counter.c.

183 {
184 if (!counter || !counter->measuring)
185 return;
186 counter->frame_write_ns += time_get_ns() - counter->write_start;
187 counter->failed |= !complete;
188 counter->written |= complete;
189}

References fps_counter_s::failed, fps_counter_s::frame_write_ns, fps_counter_s::measuring, fps_counter_s::write_start, and fps_counter_s::written.

Referenced by ui_controller_write().

◆ keyboard_help_check_signal_cancel()

bool keyboard_help_check_signal_cancel ( void )

#include <include/ascii-chat/tui/keyboard_help.h>

Check if help screen closure was requested by signal handler.

Returns
true if signal requested help closure (and clears the flag), false otherwise

Called by render loop to detect and handle Ctrl+C while help is active. Uses atomic compare-and-swap to clear flag while returning the previous value.

Note
Thread-safe using atomic operations

Definition at line 1480 of file src/common/session/display.c.

1480 {
1481 bool expected = true;
1482 return atomic_cas_bool(&g_help_signal_cancel, &expected, false);
1483}

Referenced by session_render_loop(), and ui_input_read_key().

◆ keyboard_help_is_active()

bool keyboard_help_is_active ( session_display_ctx_t * ctx)

#include <include/ascii-chat/tui/keyboard_help.h>

Check if keyboard help is currently active.

Parameters
ctxDisplay context (must not be NULL)
Returns
true if keyboard help is active, false otherwise

Non-blocking check of keyboard help state.

Note
Thread-safe: Uses atomic load

Definition at line 1485 of file src/common/session/display.c.

1485 {
1486 if (!ctx) {
1487 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL");
1488 return false;
1489 }
1490
1491 // Disable help in snapshot mode only - allow help state check even if stdin/stdout aren't TTYs
1492 if (GET_OPTION(snapshot_mode)) {
1493 return false;
1494 }
1495
1497}
#define SET_ERRNO(code,...)
@ ERROR_INVALID_PARAM
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
int ui_controller_interactive_screen(void)
@ UI_SCREEN_HELP

References ERROR_INVALID_PARAM, GET_OPTION, SET_ERRNO, ui_controller_interactive_screen(), and UI_SCREEN_HELP.

Referenced by display_render_frame(), and keyboard_help_render().

◆ keyboard_help_is_active_global()

bool keyboard_help_is_active_global ( void )

#include <include/ascii-chat/tui/keyboard_help.h>

Check if keyboard help is active (global accessor for signal handlers).

Returns
true if keyboard help is active, false otherwise

Signal-handler-safe version of keyboard_help_is_active() that doesn't require passing a context. Used by Ctrl+C handler to check if help is active before deciding to exit.

Note
Safe to call from signal handlers

Definition at line 1502 of file src/common/session/display.c.

1502 {
1503 session_display_ctx_t *ctx = get_current_display_ctx();
1505}
bool ui_controller_is_blocked(void)

References ui_controller_interactive_screen(), ui_controller_is_blocked(), and UI_SCREEN_HELP.

◆ keyboard_help_parent_screen()

int keyboard_help_parent_screen ( void )

#include <include/ascii-chat/tui/keyboard_help.h>

Definition at line 16 of file keyboard_help.c.

16 {
17 return atomic_load(&g_help_parent);
18}

Referenced by keyboard_help_toggle_terminal(), ui_controller_current_screen(), ui_controller_write(), and ui_input_read_key().

◆ keyboard_help_render()

void keyboard_help_render ( session_display_ctx_t * ctx)

#include <include/ascii-chat/tui/keyboard_help.h>

Render keyboard help TUI overlay.

Parameters
ctxDisplay context (must not be NULL)

Renders a centered keyboard help screen showing:

  • Keyboard shortcuts and their functions
  • Current program state values:
    • Volume (displayed as bar graph: โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘ 80%)
    • Color mode (Mono, 16-color, 256-color, Truecolor)
    • Webcam flip state (Normal or Flipped)
    • Audio status (Enabled or Disabled)
    • Mute status (in volume display)

Layout:

  • Centered horizontally: col = (term_width - box_width) / 2
  • Centered vertically: row = (term_height - box_height) / 2
  • Border: Box drawing chars (โ•”โ•โ•—โ•‘โ•šโ•) with ASCII fallback (+|-) for UTF-8-less terminals

Edge Cases:

  • Terminal too small: Shows simplified help or warning
  • Non-TTY: Skips rendering (no terminal for display)
Note
Reads live option values via GET_OPTION() - values reflect current state
Uses session_display_write_raw() for atomic terminal output

Definition at line 80 of file keyboard_help.c.

80 {
81 if (ctx && keyboard_help_is_active(ctx))
83}
bool keyboard_help_is_active(session_display_ctx_t *ctx)
Check if keyboard help is currently active.
void ui_controller_redraw(void)

References keyboard_help_is_active(), and ui_controller_redraw().

Referenced by display_render_frame().

◆ keyboard_help_screen_name()

const char * keyboard_help_screen_name ( int parent)

#include <include/ascii-chat/tui/keyboard_help.h>

Fixed telemetry identity matching the help menu rendered for this parent.

Definition at line 20 of file keyboard_help.c.

20 {
21 switch (parent) {
22 case UI_SCREEN_STATS:
23 return "stats_help";
24 case UI_SCREEN_SYNC:
25 return "sync_help";
26 case UI_SCREEN_ERRNO:
27 return "errno_help";
29 return "media_input_help";
30 case UI_SCREEN_MDNS:
31 return "mdns_help";
33 return "update_help";
34 case UI_SCREEN_ERROR:
35 return "error_help";
36 default:
37 return "help";
38 }
39}
@ UI_SCREEN_PROMPT
@ UI_SCREEN_ERRNO
@ UI_SCREEN_STATS
@ UI_SCREEN_UPDATE
@ UI_SCREEN_SYNC
@ UI_SCREEN_MDNS
@ UI_SCREEN_ERROR

References UI_SCREEN_ERRNO, UI_SCREEN_ERROR, UI_SCREEN_MDNS, UI_SCREEN_PROMPT, UI_SCREEN_STATS, UI_SCREEN_SYNC, and UI_SCREEN_UPDATE.

Referenced by keyboard_help_toggle_terminal().

◆ keyboard_help_signal_cancel()

void keyboard_help_signal_cancel ( void )

#include <include/ascii-chat/tui/keyboard_help.h>

Request help screen closure from signal handler (Ctrl+C).

Signal-handler-safe way to request that the help screen be closed. Sets an atomic flag that the render loop checks. The render loop then toggles off the help screen and continues running.

Note
Safe to call from signal handlers

Definition at line 1475 of file src/common/session/display.c.

1475 {
1476 atomic_store_bool(&g_help_signal_cancel, true);
1477}

Referenced by keyboard_help_toggle_global().

◆ keyboard_help_toggle()

void keyboard_help_toggle ( session_display_ctx_t * ctx)

#include <include/ascii-chat/tui/keyboard_help.h>

Toggle keyboard help on/off.

Parameters
ctxDisplay context (must not be NULL)

Atomically toggles the keyboard help active state. When active, the keyboard help is displayed and frame rendering is suppressed. Network reception continues in the background.

Note
Shares the controller selection with logs, sync and media input

Definition at line 75 of file keyboard_help.c.

75 {
76 if (ctx)
77 keyboard_help_toggle_terminal(STDOUT_FILENO);
78}
void keyboard_help_toggle_terminal(int fd)

References keyboard_help_toggle_terminal().

Referenced by session_render_loop().

◆ keyboard_help_toggle_global()

void keyboard_help_toggle_global ( void )

#include <include/ascii-chat/tui/keyboard_help.h>

Toggle keyboard help (global accessor for signal handlers).

Signal-handler-safe version of keyboard_help_toggle() that doesn't require passing a context. Used by Ctrl+C handler to close the help screen instead of exiting the app.

Note
Safe to call from signal handlers

Definition at line 1507 of file src/common/session/display.c.

1507 {
1509}
void keyboard_help_signal_cancel(void)
Request help screen closure from signal handler (Ctrl+C).

References keyboard_help_signal_cancel().

◆ keyboard_help_toggle_terminal()

void keyboard_help_toggle_terminal ( int fd)

#include <include/ascii-chat/tui/keyboard_help.h>

Shared mode-aware help without a media display dependency.

Definition at line 51 of file keyboard_help.c.

51 {
52 int parent =
54 ascii_otel_ui_usage(keyboard_help_screen_name(parent), "shortcut", 0);
56 // Keep the underlying worker active throughout dismissal. An intermediate
57 // empty selection could make it retire its screen after we restore it.
59 return;
60 }
61 if (parent == UI_SCREEN_ERROR)
62 ascii_otel_ui_usage("error", "help_viewed", 0);
63 atomic_store(&g_help_previous, ui_controller_interactive_screen());
64 atomic_store(&g_help_parent, ui_controller_current_screen());
65 terminal_size_t minimum;
66 char *text =
68 if (!text)
69 return;
70 if (!GET_OPTION(snapshot_mode))
71 (void)ui_controller_submit(UI_SCREEN_HELP, fd, minimum, render_help_snapshot, &fd, sizeof(fd));
72 SAFE_FREE(text);
73}
int keyboard_help_parent_screen(void)
const char * keyboard_help_screen_name(int parent)
int keyboard_help_previous_interactive(void)
void ascii_otel_ui_usage(const char *screen, const char *event, uint64_t duration_ns)
Definition otel.c:434
char * ui_session_context_help_render(terminal_size_t size, ui_screen_t screen, terminal_size_t *minimum)
void ui_controller_dismiss(ui_screen_t screen, int previous)
asciichat_error_t ui_controller_submit(ui_screen_t screen, int fd, terminal_size_t minimum, ui_render_fn render, const void *snapshot, size_t bytes)
int ui_controller_current_screen(void)
terminal_size_t ui_controller_size(void)
ui_screen_t

References ascii_otel_ui_usage(), GET_OPTION, keyboard_help_parent_screen(), keyboard_help_previous_interactive(), keyboard_help_screen_name(), SAFE_FREE, ui_controller_current_screen(), ui_controller_dismiss(), ui_controller_interactive_screen(), ui_controller_size(), ui_controller_submit(), UI_SCREEN_ERROR, UI_SCREEN_HELP, and ui_session_context_help_render().

Referenced by keyboard_help_toggle(), ui_controller_first_use_key(), and ui_input_read_key().

◆ session_audio_add_source()

asciichat_error_t session_audio_add_source ( session_audio_ctx_t * ctx,
uint32_t source_id )

#include <src/common/session/audio.h>

Add an audio source for mixing (host only).

Parameters
ctxAudio context (must not be NULL, must be host context)
source_idUnique identifier for this source
Returns
ASCIICHAT_OK on success, error code on failure

Registers a new audio source for the mixer. Each participant should have a unique source_id.

Note
Only available when context was created with is_host=true.

Definition at line 243 of file src/common/session/audio.c.

243 {
244 if (!ctx || !ctx->initialized) {
245 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid context");
246 }
247
248 if (!ctx->is_host) {
249 return SET_ERRNO(ERROR_INVALID_STATE, "not a host context");
250 }
251
252 // Check if source already exists
253 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
254 if (ctx->sources[i].active && ctx->sources[i].source_id == source_id) {
255 return ASCIICHAT_OK; // Already registered
256 }
257 }
258
259 // Find empty slot
260 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
261 if (!ctx->sources[i].active) {
262 ctx->sources[i].source_id = source_id;
263 ctx->sources[i].active = true;
265 if (!ctx->sources[i].buffer) {
266 ctx->sources[i].active = false;
267 return SET_ERRNO(ERROR_MEMORY, "Failed to create audio buffer for source");
268 }
269 ctx->source_count++;
270 return ASCIICHAT_OK;
271 }
272 }
273
274 return SET_ERRNO(ERROR_RESOURCE_EXHAUSTED, "Maximum audio sources reached");
275}
#define AUDIO_CHANNELS
Definition audio/limits.h:5
@ ERROR_INVALID_STATE
@ ERROR_RESOURCE_EXHAUSTED
@ ERROR_MEMORY
Definition error_codes.h:51
audio_ring_buffer_t * audio_ring_buffer_create_format(unsigned channels, bool capture)
#define SESSION_AUDIO_MAX_SOURCES
One participant's audio input in the host mixer.
int source_count
Number of active sources (host only).
bool initialized
Context is fully initialized.
bool is_host
True if this is a host context (has mixing capabilities).
session_audio_source_t sources[MAX_SESSION_PARTICIPANTS]
Audio sources for mixing (host only).

References session_audio_source_t::active, ASCIICHAT_OK, AUDIO_CHANNELS, audio_ring_buffer_create_format(), session_audio_source_t::buffer, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_MEMORY, ERROR_RESOURCE_EXHAUSTED, session_audio_ctx::initialized, session_audio_ctx::is_host, SESSION_AUDIO_MAX_SOURCES, SET_ERRNO, session_audio_ctx::source_count, session_audio_source_t::source_id, and session_audio_ctx::sources.

◆ session_audio_create()

session_audio_ctx_t * session_audio_create ( bool is_host)

#include <src/common/session/audio.h>

Create a new session audio context.

Parameters
is_hosttrue if this context is for a session host (enables mixing)
Returns
Pointer to audio context, or NULL on failure

Creates and initializes a session audio context. Hosts get audio mixing capabilities while participants only get basic capture/playback.

Note
Call session_audio_destroy() to free resources when done.
On failure, sets asciichat_errno with error details.

Definition at line 88 of file src/common/session/audio.c.

88 {
89 // Allocate context
91
92 ctx->is_host = is_host;
93 ctx->running = false;
94
95 // Initialize underlying audio context
97 if (result != ASCIICHAT_OK) {
98 log_error("Failed to initialize audio context: %d", result);
99 SAFE_FREE(ctx);
100 return NULL;
101 }
102
103 // Initialize mixing resources for host
104 if (is_host) {
105 // Allocate mix buffer (enough for one audio buffer worth)
106 ctx->mix_buffer_size = AUDIO_BUFFER_SIZE * 4; // Extra space for safety
107 ctx->mix_buffer = SAFE_CALLOC(ctx->mix_buffer_size, sizeof(float), float *);
108
109 // Initialize source array
110 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
111 ctx->sources[i].source_id = 0;
112 ctx->sources[i].active = false;
113 ctx->sources[i].buffer = NULL;
114 }
115 ctx->source_count = 0;
116 }
117
118 ctx->initialized = true;
119 return ctx;
120}
#define AUDIO_BUFFER_SIZE
asciichat_error_t audio_init(audio_context_t *ctx)
Initialize audio context and PortAudio.
size_t mix_buffer_size
Size of mix buffer in samples.
audio_context_t audio_ctx
Underlying audio context from lib/audio.
bool running
Audio streams are currently running.
float * mix_buffer
Temporary buffer for mixing operations.

References session_audio_source_t::active, ASCIICHAT_OK, AUDIO_BUFFER_SIZE, session_audio_ctx::audio_ctx, audio_init(), session_audio_source_t::buffer, session_audio_ctx::initialized, session_audio_ctx::is_host, log_error, session_audio_ctx::mix_buffer, session_audio_ctx::mix_buffer_size, session_audio_ctx::running, SAFE_CALLOC, SAFE_FREE, SESSION_AUDIO_MAX_SOURCES, session_audio_ctx::source_count, session_audio_source_t::source_id, and session_audio_ctx::sources.

Referenced by session_host_start_render(), and session_participant_start_audio().

◆ session_audio_destroy()

void session_audio_destroy ( session_audio_ctx_t * ctx)

#include <src/common/session/audio.h>

Destroy session audio context and free resources.

Parameters
ctxAudio context to destroy (can be NULL)

Stops all audio streams, cleans up mixers (if host), and releases all resources. Safe to call with NULL.

Definition at line 122 of file src/common/session/audio.c.

122 {
123 if (!ctx) {
124 return;
125 }
126
127 // Stop audio if running
128 if (ctx->running) {
130 }
131
132 // Cleanup host-specific resources
133 if (ctx->is_host) {
134 // Destroy source buffers
135 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
136 if (ctx->sources[i].buffer) {
138 ctx->sources[i].buffer = NULL;
139 }
140 }
141
142 // Free mix buffer
143 if (ctx->mix_buffer) {
144 SAFE_FREE(ctx->mix_buffer);
145 }
146 }
147
148 // Destroy underlying audio context
151
152 ctx->initialized = false;
153 SAFE_FREE(ctx);
154}
void client_audio_pipeline_destroy(client_audio_pipeline_t *pipeline)
Destroy a client audio pipeline.
void audio_ring_buffer_destroy(audio_ring_buffer_t *rb)
Destroy an audio ring buffer.
void audio_destroy(audio_context_t *ctx)
Destroy audio context and clean up resources.
void session_audio_stop(session_audio_ctx_t *ctx)
Stop all audio streams.
client_audio_pipeline_t * pipeline

References session_audio_ctx::audio_ctx, audio_destroy(), audio_ring_buffer_destroy(), session_audio_source_t::buffer, client_audio_pipeline_destroy(), session_audio_ctx::initialized, session_audio_ctx::is_host, session_audio_ctx::mix_buffer, session_audio_ctx::pipeline, session_audio_ctx::running, SAFE_FREE, SESSION_AUDIO_MAX_SOURCES, session_audio_stop(), and session_audio_ctx::sources.

Referenced by session_host_destroy(), session_host_stop_render(), and session_participant_stop_audio().

◆ session_audio_is_running()

bool session_audio_is_running ( session_audio_ctx_t * ctx)

#include <src/common/session/audio.h>

Check if audio is currently running.

Parameters
ctxAudio context (can be NULL)
Returns
true if audio is active, false otherwise

Definition at line 212 of file src/common/session/audio.c.

212 {
213 if (!ctx || !ctx->initialized) {
214 return false;
215 }
216 return ctx->running;
217}

References session_audio_ctx::initialized, and session_audio_ctx::running.

◆ session_audio_mix_excluding()

size_t session_audio_mix_excluding ( session_audio_ctx_t * ctx,
uint32_t exclude_id,
float * output,
size_t num_samples )

#include <src/common/session/audio.h>

Mix all sources except one and return the result (host only).

Parameters
ctxAudio context (must not be NULL, must be host context)
exclude_idSource ID to exclude from mix (for echo prevention)
outputOutput buffer for mixed audio (must not be NULL)
num_samplesNumber of samples to mix
Returns
Number of samples actually mixed

Mixes audio from all registered sources except exclude_id. This allows sending each participant a mix of all other participants without their own audio (preventing echo).

Note
Only available when context was created with is_host=true.

Definition at line 319 of file src/common/session/audio.c.

319 {
320 if (!ctx || !ctx->initialized || !output || num_samples == 0) {
321 return 0;
322 }
323
324 if (!ctx->is_host) {
325 return 0;
326 }
327
328 // Initialize output to silence
329 memset(output, 0, num_samples * sizeof(float));
330
331 // Ensure we have a mix buffer
332 if (!ctx->mix_buffer || num_samples > ctx->mix_buffer_size) {
333 return 0;
334 }
335
336 int sources_mixed = 0;
337
338 // Mix all active sources except the excluded one
339 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
340 if (!ctx->sources[i].active || ctx->sources[i].source_id == exclude_id) {
341 continue;
342 }
343
344 if (!ctx->sources[i].buffer) {
345 continue;
346 }
347
348 // Read samples from this source
349 size_t samples_read = audio_ring_buffer_read(ctx->sources[i].buffer, ctx->mix_buffer, num_samples);
350
351 if (samples_read == 0) {
352 continue;
353 }
354
355 // Add to output mix
356 for (size_t j = 0; j < samples_read; j++) {
357 output[j] += ctx->mix_buffer[j];
358 }
359
360 sources_mixed++;
361 }
362
363 // Apply simple clipping prevention if multiple sources were mixed
364 if (sources_mixed > 1) {
365 float scale = 1.0f / (float)sources_mixed;
366 for (size_t i = 0; i < num_samples; i++) {
367 output[i] *= scale;
368 // Soft clipping
369 if (output[i] > 1.0f) {
370 output[i] = 1.0f;
371 } else if (output[i] < -1.0f) {
372 output[i] = -1.0f;
373 }
374 }
375 }
376
377 return num_samples;
378}
size_t audio_ring_buffer_read(audio_ring_buffer_t *rb, float *data, size_t samples)
Read audio samples from ring buffer.

References session_audio_source_t::active, audio_ring_buffer_read(), session_audio_source_t::buffer, session_audio_ctx::initialized, session_audio_ctx::is_host, session_audio_ctx::mix_buffer, session_audio_ctx::mix_buffer_size, SESSION_AUDIO_MAX_SOURCES, session_audio_source_t::source_id, and session_audio_ctx::sources.

◆ session_audio_read_captured()

size_t session_audio_read_captured ( session_audio_ctx_t * ctx,
float * buffer,
size_t num_samples )

#include <src/common/session/audio.h>

Read captured audio samples.

Parameters
ctxAudio context (must not be NULL)
bufferOutput buffer for audio samples (must not be NULL)
num_samplesMaximum number of samples to read
Returns
Number of samples actually read

Reads captured audio samples from the capture ring buffer. Samples are in float format (-1.0 to 1.0 range).

Definition at line 223 of file src/common/session/audio.c.

223 {
224 if (!ctx || !ctx->initialized || !buffer || num_samples == 0) {
225 return 0;
226 }
227
228 return audio_ring_buffer_read(ctx->audio_ctx.capture_buffer, buffer, num_samples);
229}
audio_ring_buffer_t * capture_buffer
OLD: Worker โ†’ Network (will become processed_capture_rb).

References session_audio_ctx::audio_ctx, audio_ring_buffer_read(), audio_context_t::capture_buffer, and session_audio_ctx::initialized.

◆ session_audio_remove_source()

void session_audio_remove_source ( session_audio_ctx_t * ctx,
uint32_t source_id )

#include <src/common/session/audio.h>

Remove an audio source from mixing (host only).

Parameters
ctxAudio context (must not be NULL, must be host context)
source_idSource identifier to remove

Unregisters an audio source from the mixer.

Note
Only available when context was created with is_host=true.

Definition at line 277 of file src/common/session/audio.c.

277 {
278 if (!ctx || !ctx->initialized || !ctx->is_host) {
279 return;
280 }
281
282 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
283 if (ctx->sources[i].active && ctx->sources[i].source_id == source_id) {
284 if (ctx->sources[i].buffer) {
286 ctx->sources[i].buffer = NULL;
287 }
288 ctx->sources[i].active = false;
289 ctx->sources[i].source_id = 0;
290 ctx->source_count--;
291 return;
292 }
293 }
294}

References session_audio_source_t::active, audio_ring_buffer_destroy(), session_audio_source_t::buffer, session_audio_ctx::initialized, session_audio_ctx::is_host, SESSION_AUDIO_MAX_SOURCES, session_audio_ctx::source_count, session_audio_source_t::source_id, and session_audio_ctx::sources.

◆ session_audio_start_capture()

asciichat_error_t session_audio_start_capture ( session_audio_ctx_t * ctx)

#include <src/common/session/audio.h>

Start audio capture (microphone input).

Parameters
ctxAudio context (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Starts capturing audio from the default input device. Captured audio is available for transmission via session_audio_read_captured().

Definition at line 160 of file src/common/session/audio.c.

160 {
161 if (!ctx || !ctx->initialized) {
162 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid context");
163 }
164
165 // Use full duplex for proper echo cancellation
166 return session_audio_start_duplex(ctx);
167}
asciichat_error_t session_audio_start_duplex(session_audio_ctx_t *ctx)
Start full-duplex audio (simultaneous capture and playback).

References ERROR_INVALID_PARAM, session_audio_ctx::initialized, session_audio_start_duplex(), and SET_ERRNO.

◆ session_audio_start_duplex()

asciichat_error_t session_audio_start_duplex ( session_audio_ctx_t * ctx)

#include <src/common/session/audio.h>

Start full-duplex audio (simultaneous capture and playback).

Parameters
ctxAudio context (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Starts both capture and playback in a single full-duplex stream. This is preferred over separate start_capture/start_playback calls as it provides better echo cancellation support.

Definition at line 178 of file src/common/session/audio.c.

178 {
179 if (!ctx || !ctx->initialized) {
180 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid context");
181 }
182
183 if (ctx->running) {
184 return ASCIICHAT_OK; // Already running
185 }
186
187 if (!ctx->pipeline) {
191 if (!ctx->pipeline)
192 return SET_ERRNO(ERROR_AUDIO, "Cannot initialize session audio processing");
194 }
196 if (result == ASCIICHAT_OK) {
197 ctx->running = true;
198 }
199
200 return result;
201}
client_audio_pipeline_t * client_audio_pipeline_create(const client_audio_pipeline_config_t *config)
Create and initialize a client audio pipeline.
client_audio_pipeline_config_t client_audio_pipeline_config_for_source(bool has_media_audio)
asciichat_error_t audio_start_duplex(audio_context_t *ctx)
Start full-duplex audio (simultaneous capture and playback).
void audio_set_pipeline(audio_context_t *ctx, void *pipeline)
Set audio pipeline for echo cancellation.
@ ERROR_AUDIO
Definition error_codes.h:62
bool media_source_has_audio(media_source_t *source)
Check if media source has audio stream.
Definition source.c:801
void * capture_media_source
Borrowed local media audio; configure before starting duplex.
Pipeline configuration parameters.

References ASCIICHAT_OK, session_audio_ctx::audio_ctx, audio_set_pipeline(), audio_start_duplex(), audio_context_t::capture_media_source, client_audio_pipeline_config_for_source(), client_audio_pipeline_create(), ERROR_AUDIO, ERROR_INVALID_PARAM, session_audio_ctx::initialized, media_source_has_audio(), session_audio_ctx::pipeline, session_audio_ctx::running, and SET_ERRNO.

Referenced by session_audio_start_capture(), session_audio_start_playback(), and session_participant_start_audio().

◆ session_audio_start_playback()

asciichat_error_t session_audio_start_playback ( session_audio_ctx_t * ctx)

#include <src/common/session/audio.h>

Start audio playback (speaker output).

Parameters
ctxAudio context (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Starts playback on the default output device. Audio for playback should be provided via session_audio_write_playback().

Definition at line 169 of file src/common/session/audio.c.

169 {
170 if (!ctx || !ctx->initialized) {
171 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid context");
172 }
173
174 // Use full duplex for proper echo cancellation
175 return session_audio_start_duplex(ctx);
176}

References ERROR_INVALID_PARAM, session_audio_ctx::initialized, session_audio_start_duplex(), and SET_ERRNO.

◆ session_audio_stop()

void session_audio_stop ( session_audio_ctx_t * ctx)

#include <src/common/session/audio.h>

Stop all audio streams.

Parameters
ctxAudio context (must not be NULL)

Stops capture, playback, and any mixing operations.

Definition at line 203 of file src/common/session/audio.c.

203 {
204 if (!ctx || !ctx->initialized || !ctx->running) {
205 return;
206 }
207
208 (void)audio_stop_duplex(&ctx->audio_ctx);
209 ctx->running = false;
210}
asciichat_error_t audio_stop_duplex(audio_context_t *ctx)
Stop full-duplex audio.

References session_audio_ctx::audio_ctx, audio_stop_duplex(), session_audio_ctx::initialized, and session_audio_ctx::running.

Referenced by session_audio_destroy().

◆ session_audio_write_playback()

asciichat_error_t session_audio_write_playback ( session_audio_ctx_t * ctx,
const float * buffer,
size_t num_samples )

#include <src/common/session/audio.h>

Write audio samples for playback.

Parameters
ctxAudio context (must not be NULL)
bufferInput buffer with audio samples (must not be NULL)
num_samplesNumber of samples to write
Returns
ASCIICHAT_OK on success, error code on failure

Writes audio samples to the playback ring buffer. Samples should be in float format (-1.0 to 1.0 range).

Definition at line 231 of file src/common/session/audio.c.

231 {
232 if (!ctx || !ctx->initialized || !buffer) {
233 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid parameter");
234 }
235
236 return audio_write_samples(&ctx->audio_ctx, buffer, (int)num_samples);
237}
asciichat_error_t audio_write_samples(audio_context_t *ctx, const float *buffer, int num_samples)
Write audio samples to playback buffer.

References session_audio_ctx::audio_ctx, audio_write_samples(), ERROR_INVALID_PARAM, session_audio_ctx::initialized, and SET_ERRNO.

◆ session_audio_write_source()

asciichat_error_t session_audio_write_source ( session_audio_ctx_t * ctx,
uint32_t source_id,
const float * samples,
size_t num_samples )

#include <src/common/session/audio.h>

Write audio samples from a specific source (host only).

Parameters
ctxAudio context (must not be NULL, must be host context)
source_idSource identifier
samplesAudio samples from this source
num_samplesNumber of samples
Returns
ASCIICHAT_OK on success, error code on failure

Provides audio data from a specific participant to the mixer.

Note
Only available when context was created with is_host=true.

Definition at line 296 of file src/common/session/audio.c.

297 {
298 if (!ctx || !ctx->initialized || !samples) {
299 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid parameter");
300 }
301
302 if (!ctx->is_host) {
303 return SET_ERRNO(ERROR_INVALID_STATE, "not a host context");
304 }
305
306 // Find the source
307 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
308 if (ctx->sources[i].active && ctx->sources[i].source_id == source_id) {
309 if (ctx->sources[i].buffer) {
310 return audio_ring_buffer_write(ctx->sources[i].buffer, samples, (int)num_samples);
311 }
312 return SET_ERRNO(ERROR_INVALID_STATE, "Source buffer not initialized");
313 }
314 }
315
316 return SET_ERRNO(ERROR_NOT_FOUND, "Audio source not found: %u", source_id);
317}
asciichat_error_t audio_ring_buffer_write(audio_ring_buffer_t *rb, const float *data, int samples)
Write audio samples to ring buffer.
@ ERROR_NOT_FOUND

References session_audio_source_t::active, audio_ring_buffer_write(), session_audio_source_t::buffer, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_NOT_FOUND, session_audio_ctx::initialized, session_audio_ctx::is_host, SESSION_AUDIO_MAX_SOURCES, SET_ERRNO, session_audio_source_t::source_id, and session_audio_ctx::sources.

◆ session_capture_at_end()

bool session_capture_at_end ( session_capture_ctx_t * ctx)

#include <src/common/session/capture.h>

Check if capture source has reached end of stream.

Parameters
ctxCapture context (must not be NULL)
Returns
true if end of stream reached, false otherwise

Useful for detecting end of file sources. Webcam and test pattern sources never reach end.

Definition at line 1106 of file common/session/capture.c.

1106 {
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}
bool media_source_at_end(media_source_t *source)
Check if media source reached end of stream.
Definition source.c:836
media_source_type_t media_source_get_type(media_source_t *source)
Get media source type.
Definition source.c:999
@ MEDIA_SOURCE_FILE
Media file (video/audio).
Definition source.h:99
bool media_playlist_adjacent(const media_playlist_t *list, int direction, size_t *index)
Definition playlist.c:113
bool initialized
Context has been successfully initialized.
media_source_t * source
Underlying media source (webcam, file, stdin, test).

References session_capture_ctx::advance_failed, media_playlist_t::count, GET_OPTION, session_capture_ctx::initialized, session_capture_ctx::loader_created, media_playlist_adjacent(), media_source_at_end(), MEDIA_SOURCE_FILE, media_source_get_type(), session_capture_ctx::playlist, and session_capture_ctx::source.

◆ session_capture_create()

session_capture_ctx_t * session_capture_create ( const session_capture_config_t * config)

#include <src/common/session/capture.h>

Legacy function - creates either mirror or network capture based on options.

Maintained for backwards compatibility. New code should use session_mirror_capture_create() or session_network_capture_create() directly.

Parameters
configCapture configuration
Returns
Capture context, or NULL on error
Deprecated
Use session_mirror_capture_create() or session_network_capture_create()

Definition at line 289 of file common/session/capture.c.

289 {
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}
bool errno_peek_since(asciichat_errno_scope_t scope, asciichat_error_context_t *out)
asciichat_errno_scope_t errno_checkpoint(void)
#define CLEAR_ERRNO()
#define CAPTURE_TRACKER_NAME_BUFFER_SIZE
#define SESSION_CAPTURE_WEBCAM_INDEX_BUFFER_SIZE
void fps_init(fps_t *tracker, int expected_fps, const char *name)
Initialize FPS tracker.
Definition fps.c:32
@ ERROR_MEDIA_INIT
Definition error_codes.h:65
#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_info(...)
Log an INFO message.
Definition log/log.h:608
void media_source_destroy(media_source_t *source)
Destroy media source and free resources.
Definition source.c:508
double media_source_get_video_fps(media_source_t *source)
Get video frame rate in frames per second.
Definition source.c:1047
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
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_TEST
Test pattern generator.
Definition source.h:101
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
#define NOTICE(severity, title,...)
Definition notice.h:68
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
asciichat_error_t media_playlist_append(media_playlist_t *list, const char *path, bool url)
Definition playlist.c:7
media_source_t * media_source_create_interruptible(media_source_type_t type, const char *path, bool(*cancel)(void *), void *data)
Definition source.c:225
asciichat_error_t media_source_start_video(media_source_t *source)
Definition source.c:571
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...
char paths[64][256]
Definition playlist.h:10
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).
fps_t fps_tracker
FPS tracker for monitoring capture rate.
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.
bool using_file_audio
Using file audio (true) or microphone fallback (false).
_Atomic bool audio_enabled
Audio is enabled for capture.
uint32_t target_fps
Target frames per second.
uint64_t start_time_ns
Start time for FPS calculation (nanoseconds).
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.
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...

References ASCII_OTEL_SCOPE, ASCIICHAT_OK, session_capture_ctx::audio_enabled, session_capture_ctx::audio_fallback_enabled, session_capture_config_t::audio_fallback_to_mic, session_capture_config_t::callback_data, session_capture_ctx::callback_data, CAPTURE_TRACKER_NAME_BUFFER_SIZE, CLEAR_ERRNO, asciichat_error_context_t::code, media_playlist_t::count, DEFAULT_MAX_FPS, session_capture_config_t::enable_audio, errno_checkpoint(), errno_peek_since(), ERROR_MEDIA_INIT, session_capture_ctx::file_has_audio, fps_init(), session_capture_ctx::fps_tracker, session_capture_ctx::frame_count, GET_OPTION, session_capture_config_t::initial_seek_timestamp, session_capture_ctx::initialized, session_capture_ctx::load_mutex, log_debug, log_info, log_warn, session_capture_config_t::loop, session_capture_ctx::media_locks_ready, media_playlist_append(), media_source_create_interruptible(), media_source_destroy(), MEDIA_SOURCE_FILE, media_source_get_video_fps(), media_source_has_audio(), media_source_seek(), media_source_set_exit_callback(), media_source_set_loop(), media_source_start_video(), MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, session_capture_config_t::mic_audio_ctx, session_capture_ctx::mic_audio_ctx, NOTICE, session_capture_config_t::path, media_playlist_t::paths, session_capture_ctx::playlist, session_capture_config_t::resize_for_network, session_capture_ctx::resize_for_network, SAFE_CALLOC, SAFE_FREE, SAFE_MALLOC, safe_snprintf(), session_capture_ctx::saved_fft, session_capture_ctx::saved_matrix, session_capture_ctx::saved_waveform, SESSION_CAPTURE_WEBCAM_INDEX_BUFFER_SIZE, SET_ERRNO, session_capture_config_t::should_exit_callback, session_capture_ctx::should_exit_callback, session_capture_ctx::should_pause_after_first_frame, session_capture_ctx::source, session_capture_ctx::source_mutex, session_capture_ctx::start_time_ns, session_capture_config_t::target_fps, session_capture_ctx::target_fps, session_capture_config_t::type, and session_capture_ctx::using_file_audio.

Referenced by capture_init(), display_init(), session_mirror_capture_create(), and session_participant_start_video().

◆ session_capture_destroy()

void session_capture_destroy ( session_capture_ctx_t * ctx)

#include <src/common/session/capture.h>

Destroy session capture context and free resources.

Parameters
ctxCapture context to destroy (can be NULL)

Cleans up the capture context and releases all resources including the media source. Safe to call with NULL.

Definition at line 486 of file common/session/capture.c.

486 {
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}
void face_follow_image_destroy(face_follow_image_t *context)
void image_destroy(image_t *image)
Destroy an image allocated with image_new().
const char * tracker_name
Definition fps.h:59
media_source_t * loaded_source
face_follow_image_t * face_follow
void ui_controller_remove(ui_screen_t screen)
@ UI_SCREEN_RENDER_PROGRESS

References ASCII_OTEL_SCOPE, session_capture_ctx::empty_frame, session_capture_ctx::face_follow, face_follow_image_destroy(), session_capture_ctx::fps_tracker, image_destroy(), session_capture_ctx::initial_frame, session_capture_ctx::initialized, session_capture_ctx::last_media_frame, session_capture_ctx::load_cancel, session_capture_ctx::load_mutex, session_capture_ctx::loaded_frame, session_capture_ctx::loaded_source, session_capture_ctx::loader, session_capture_ctx::loader_created, log_error, session_capture_ctx::media_locks_ready, media_source_destroy(), SAFE_FREE, session_capture_ctx::source, session_capture_ctx::source_mutex, fps_t::tracker_name, ui_controller_remove(), UI_SCREEN_RENDER_PROGRESS, and session_capture_ctx::webcam_source.

Referenced by capture_cleanup(), display_cleanup(), and session_participant_stop_video().

◆ session_capture_get_current_fps()

double session_capture_get_current_fps ( session_capture_ctx_t * ctx)

#include <src/common/session/capture.h>

Get the current FPS being achieved by the capture source.

Parameters
ctxCapture context (must not be NULL)
Returns
Current FPS, or 0.0 if not enough frames captured yet

Definition at line 1126 of file common/session/capture.c.

1126 {
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}
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition time.c:123

References session_capture_ctx::frame_count, session_capture_ctx::initialized, session_capture_ctx::start_time_ns, and time_elapsed_ns().

◆ session_capture_get_media_source()

void * session_capture_get_media_source ( session_capture_ctx_t * ctx)

#include <src/common/session/capture.h>

Get the underlying media source from capture context.

Parameters
ctxCapture context (must not be NULL)
Returns
Pointer to media source (cast to media_source_t*), or NULL if not available

Returns the underlying media_source_t for operations like seeking, pause/resume, or checking media type. Used by audio playback and interactive controls (keyboard input).

Note
Pointer is owned by the capture context and should not be freed
Use for advanced control like pause/resume or seek operations

Definition at line 1197 of file common/session/capture.c.

1197 {
1198 if (!ctx || !ctx->initialized || !ctx->source) {
1199 return NULL;
1200 }
1201 return (void *)ctx->source;
1202}

References session_capture_ctx::initialized, and session_capture_ctx::source.

Referenced by audio_connection_init(), capture_get_media_source(), client_protocol_start(), session_participant_start_audio(), and session_pipeline_create().

◆ session_capture_get_target_fps()

uint32_t session_capture_get_target_fps ( session_capture_ctx_t * ctx)

#include <src/common/session/capture.h>

Get the target FPS configured for this capture context.

Parameters
ctxCapture context (must not be NULL)
Returns
Target FPS from configuration

Definition at line 1142 of file common/session/capture.c.

1142 {
1143 if (!ctx) {
1144 return 0;
1145 }
1146 return ctx->target_fps;
1147}

References session_capture_ctx::target_fps.

◆ session_capture_has_audio()

bool session_capture_has_audio ( session_capture_ctx_t * ctx)

#include <src/common/session/capture.h>

Check if capture source has audio available.

Parameters
ctxCapture context (must not be NULL)
Returns
true if audio is available, false otherwise

Returns whether the capture source has audio and it is enabled.

Definition at line 1149 of file common/session/capture.c.

1149 {
1150 if (!ctx || !ctx->initialized) {
1151 return false;
1152 }
1153 return ctx->audio_enabled;
1154}

References session_capture_ctx::audio_enabled, and session_capture_ctx::initialized.

◆ session_capture_is_valid()

bool session_capture_is_valid ( session_capture_ctx_t * ctx)

#include <src/common/session/capture.h>

Check if capture context is initialized and valid.

Parameters
ctxCapture context (can be NULL)
Returns
true if context is valid and initialized, false otherwise

Definition at line 1122 of file common/session/capture.c.

1122 {
1123 return ctx != NULL && ctx->initialized && ctx->source != NULL;
1124}

References session_capture_ctx::initialized, and session_capture_ctx::source.

◆ session_capture_process_for_transmission()

image_t * session_capture_process_for_transmission ( session_capture_ctx_t * ctx,
image_t * frame )

#include <src/common/session/capture.h>

Process a frame for network transmission (resize if needed).

Parameters
ctxCapture context (must not be NULL)
frameInput frame from session_capture_read_frame() (must not be NULL)
Returns
Processed image ready for transmission, or NULL on error

Resizes the frame to network-optimal dimensions if resize_for_network was enabled in the configuration. The returned image is a new allocation owned by the caller and must be freed with image_destroy().

Note
Returns a copy if no resizing is needed.
Caller owns the returned image and must call image_destroy().

Definition at line 1083 of file common/session/capture.c.

1083 {
1084 image_t *output = NULL;
1086 return NULL;
1087 return output;
1088}
asciichat_error_t session_capture_process_for_transmission_into(session_capture_ctx_t *ctx, image_t *frame, image_t **output)

References ASCIICHAT_OK, and session_capture_process_for_transmission_into().

◆ session_capture_read_audio()

size_t session_capture_read_audio ( session_capture_ctx_t * ctx,
float * buffer,
size_t num_samples )

#include <src/common/session/capture.h>

Read audio samples from capture source.

Parameters
ctxCapture context (must not be NULL)
bufferOutput buffer for audio samples (must not be NULL)
num_samplesNumber of samples to read
Returns
Number of samples actually read (0 if no audio available)

Reads audio samples from the media source. Audio is read from either:

  • File if available and enabled
  • Microphone if using fallback

Returns 0 if audio is not enabled or available.

Definition at line 1182 of file common/session/capture.c.

1182 {
1183 ASCII_OTEL_SCOPE(SESSION_CAPTURE_READ_AUDIO);
1185 size_t count = capture_read_audio(ctx, buffer, num_samples);
1187 return count;
1188}
void session_capture_lock_source(session_capture_ctx_t *ctx)
void session_capture_unlock_source(session_capture_ctx_t *ctx)

References ASCII_OTEL_SCOPE, session_capture_lock_source(), and session_capture_unlock_source().

◆ session_capture_read_frame()

image_t * session_capture_read_frame ( session_capture_ctx_t * ctx)

#include <src/common/session/capture.h>

Read the next video frame from the capture source.

Parameters
ctxCapture context (must not be NULL)
Returns
Pointer to image frame, or NULL on error/end

Reads the next video frame from the media source. The returned image is owned by the media source and should NOT be freed by the caller.

Note
Frame is valid until next call or context destruction.
Check session_capture_at_end() to distinguish error from EOF.

Definition at line 1048 of file common/session/capture.c.

1048 {
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}

References ASCII_OTEL_SCOPE, session_capture_lock_source(), and session_capture_unlock_source().

◆ session_capture_sleep_for_fps()

void session_capture_sleep_for_fps ( session_capture_ctx_t * ctx)

#include <src/common/session/capture.h>

Sleep to maintain target frame rate.

Parameters
ctxCapture context (must not be NULL)

Implements adaptive sleep to maintain the configured target frame rate. Call this once per frame capture iteration.

Note
Uses adaptive sleep for smooth frame rate limiting.

Definition at line 1090 of file common/session/capture.c.

1090 {
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}
uint64_t next_frame_deadline_ns
Next capture deadline on the monotonic clock (nanoseconds).

References session_capture_ctx::initialized, session_capture_ctx::next_frame_deadline_ns, NS_PER_SEC_INT, session_capture_ctx::source, and session_capture_ctx::target_fps.

◆ session_capture_using_file_audio()

bool session_capture_using_file_audio ( session_capture_ctx_t * ctx)

#include <src/common/session/capture.h>

Check if currently using file audio vs microphone fallback.

Parameters
ctxCapture context (must not be NULL)
Returns
true if using file audio, false if using microphone or no audio

Useful for logging and debugging which audio source is active.

Definition at line 1190 of file common/session/capture.c.

1190 {
1191 if (!ctx || !ctx->initialized) {
1192 return false;
1193 }
1194 return ctx->using_file_audio;
1195}

References session_capture_ctx::initialized, and session_capture_ctx::using_file_audio.

◆ session_client_like_get_stdin_reader()

terminal_fd_reader_t * session_client_like_get_stdin_reader ( void )

#include <src/common/session/client_like.h>

Get the stdin frame reader for ASCII-to-video rendering (stdin render mode only).

Returns the stdin reader created when stdin render mode is active. Only valid after session_client_like_run() is called and during run_fn execution. May be NULL if not in stdin render mode.

Returns
Pointer to terminal_fd_reader_t, or NULL if not in stdin render mode

Definition at line 105 of file client_like.c.

105 {
106 return g_stdin_reader;
107}

◆ session_display_begin_drain()

void session_display_begin_drain ( session_display_ctx_t * ctx,
uint64_t total,
bool total_known )

#include <src/common/session/display.h>

Definition at line 416 of file src/common/session/display.c.

416 {
417 if (ctx && ctx->render_file) {
419 render_file_begin_drain(ctx->render_file, total, total_known);
420 }
421}
void render_file_begin_drain(render_file_ctx_t *ctx, uint64_t total, bool total_known)
Definition renderer.c:364
render_file_ctx_t * render_file
Render-to-file context (NULL if disabled).

References session_display_ctx::render_file, render_file_begin_drain(), ui_controller_remove(), and UI_SCREEN_HELP.

Referenced by session_pipeline_destroy().

◆ session_display_clear()

void session_display_clear ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Clear the terminal screen.

Parameters
ctxDisplay context (must not be NULL)

Clears the terminal screen and moves cursor to home position.

Definition at line 1370 of file src/common/session/display.c.

1370 {
1371 if (ctx && !ctx->snapshot_mode)
1373}
bool snapshot_mode
Snapshot mode enabled.

References session_display_ctx::snapshot_mode, and ui_controller_redraw().

◆ session_display_convert_to_ascii()

char * session_display_convert_to_ascii ( session_display_ctx_t * ctx,
const image_t * image )

#include <src/common/session/display.h>

Convert an image to ASCII art using display context and command-line options.

Parameters
ctxDisplay context (must not be NULL)
imageImage to convert (must not be NULL)
Returns
Dynamically allocated ASCII string, or NULL on error

Converts the given image to ASCII art using:

  • Palette and terminal capabilities from display context
  • Width, height, stretch, and aspect ratio settings from GET_OPTION()

This completely encapsulates ASCII conversion complexity so callers don't need to manage palette, terminal capabilities, or conversion options.

The returned string must be freed by caller with SAFE_FREE().

Definition at line 928 of file src/common/session/display.c.

928 {
929 return convert_to_ascii(ctx, image, false);
930}

Referenced by session_display_encode_frame().

◆ session_display_create()

session_display_ctx_t * session_display_create ( const session_display_config_t * config)

#include <src/common/session/display.h>

Create a new session display context.

Parameters
configDisplay configuration (must not be NULL)
Returns
Pointer to display context, or NULL on failure

Creates and initializes a session display context with the specified configuration. Detects terminal capabilities and initializes palette.

Note
Call session_display_destroy() to free resources when done.
On failure, sets asciichat_errno with error details.

Definition at line 224 of file src/common/session/display.c.

224 {
225 ASCII_OTEL_SCOPE(SESSION_DISPLAY_CREATE);
226 if (GET_OPTION(zalgo) > 0 && (GET_OPTION(force_utf8) == UTF8_SETTING_FALSE ||
227 (GET_OPTION(force_utf8) == UTF8_SETTING_AUTO && terminal_is_stdout_tty() &&
228 !detect_terminal_capabilities(options_terminal_settings()).utf8_support))) {
229 SET_ERRNO(ERROR_USAGE, "--zalgo requires UTF-8 terminal output (on Windows, run chcp 65001 first)");
230 return NULL;
231 }
232 // Auto-create config from command-line options if NULL
233 session_display_config_t auto_config = {0};
234 if (!config) {
235 auto_config.snapshot_mode = GET_OPTION(snapshot_mode);
236 auto_config.palette_type = GET_OPTION(palette_type);
237 auto_config.custom_palette = GET_OPTION(palette_custom_set) ? GET_OPTION(palette_custom) : NULL;
238 auto_config.color_mode = TERM_COLOR_AUTO;
239 config = &auto_config;
240 }
241
242 // Check if we should exit before starting initialization
243 if (config->should_exit_callback && config->should_exit_callback(config->callback_data)) {
244 return NULL;
245 }
246
247 // Allocate context
249
250 // Store configuration
251 ctx->snapshot_mode = config->snapshot_mode;
252 ctx->palette_type = config->palette_type;
254 ctx->audio_ctx = config->audio_ctx;
255 ctx->render_fps = config->render_fps;
256 atomic_store_bool(&ctx->first_frame, true);
257 atomic_store_bool(&ctx->output_failed, false);
258
259 // Get TTY info for direct terminal access
260 ctx->tty_info = get_current_tty();
261
262 // Presentation follows stdout. Opening a separate controlling terminal can
263 // fail under ConPTY even though stdout is an interactive console.
264 ctx->has_tty = terminal_is_stdout_tty();
265 const char *stdout_output = GET_OPTION(render_file);
266 bool owns_stdout = GET_OPTION(render_kitty) || strcmp(stdout_output, "--kitty") == 0 ||
267 strcmp(stdout_output, "-") == 0 || strcmp(stdout_output, "pipe:") == 0;
268 if (owns_stdout)
269 ctx->has_tty = false;
270 if (ctx->has_tty) {
271 // Retain the terminal before decoder/audio workers redirect process stdio
272 // through LOG_IO. The first media submission must remain interactive too.
273 int output = platform_dup(STDOUT_FILENO);
274 if (output >= 0 && platform_isatty(output)) {
275 if (ctx->tty_info.owns_fd && ctx->tty_info.fd >= 0)
276 platform_close(ctx->tty_info.fd);
277 ctx->tty_info.fd = output;
278 ctx->tty_info.owns_fd = true;
279 } else if (output >= 0) {
280 platform_close(output);
281 }
282 }
283 if (ctx->has_tty && (ctx->tty_info.fd < 0 || !platform_isatty(ctx->tty_info.fd))) {
284 if (ctx->tty_info.owns_fd && ctx->tty_info.fd >= 0)
285 platform_close(ctx->tty_info.fd);
286 ctx->tty_info.fd = STDOUT_FILENO;
287 ctx->tty_info.owns_fd = false;
288 }
289
290 // In piped mode, force all logs to stderr to prevent frame data corruption
291 if (terminal_should_force_stderr()) {
293 // Disable buffering for stdout to ensure frames appear immediately
294 // Each frame ends with newline, so unbuffered output is critical for real-time display
295 (void)setvbuf(stdout, NULL, _IONBF, 0);
296 }
297
298 // Also disable buffering for TTY mode to ensure smooth 40+ FPS animation
299 setvbuf(stdout, NULL, _IONBF, 0);
300
301 // Detect terminal capabilities
302 ctx->caps = detect_terminal_capabilities(options_terminal_settings());
303
304 // Set wants_padding based on terminal output mode
305 // Enable padding for all rendering modes (including snapshot) to center output
306 // The padding adds newlines at the top, which is useful for visual centering
307 bool is_snapshot_mode = config->snapshot_mode;
308 ctx->caps.wants_padding = true;
309
310 // Calculate pad_height: when padding is enabled, add top padding for centering
311 // Halfblock mode uses 2 source rows per output row, so padding is halved
312 if (ctx->caps.wants_padding) {
313 ctx->caps.pad_height = 5; // Halfblock needs half the padding (10 / 2 = 5)
314 } else {
315 ctx->caps.pad_height = 0;
316 }
317
318 log_debug("Padding mode: wants_padding=%d (snapshot=%d, has_tty=%d, stdin_tty=%d, stdout_tty=%d), pad_height=%zu",
319 ctx->caps.wants_padding, is_snapshot_mode, ctx->has_tty, terminal_is_stdin_tty(), terminal_is_stdout_tty(),
320 ctx->caps.pad_height);
321
322 // Apply color mode override if specified
323 if (config->color_mode != TERM_COLOR_AUTO) {
324 ctx->caps.color_level = config->color_mode;
325 }
326
327 // Apply command-line overrides
328 ctx->caps = apply_color_mode_override(ctx->caps, options_terminal_settings());
329
330 // Initialize palette
331 int palette_result = initialize_client_palette(config->palette_type, config->custom_palette, ctx->palette_chars,
332 &ctx->palette_len, ctx->luminance_palette);
333 if (palette_result != ASCIICHAT_OK) {
334 log_warn("Failed to initialize palette, using default");
335 // Fall back to standard palette
337 ctx->luminance_palette);
338 }
339
340 // Pre-warm UTF-8 palette cache during initialization (not during rendering)
341 // This is expensive on first use (creates lookup tables), so warm it up now to avoid frame lag
342 // This function is imported from lib/video/simd/common.h
343 (void)get_utf8_palette_cache((const char *)ctx->palette_chars);
344 log_debug("UTF-8 palette cache pre-warmed during display initialization");
345
346 // Initialize ANSI fast lookup tables based on terminal capabilities
347 if (ctx->caps.color_level == TERM_COLOR_TRUECOLOR) {
349 } else if (ctx->caps.color_level == TERM_COLOR_256) {
351 } else if (ctx->caps.color_level == TERM_COLOR_16) {
353 }
354 // TERM_COLOR_MONO requires no init
355
356 // Initialize ASCII output system if we have a TTY
357 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
358 ascii_write_init(ctx->tty_info.fd, false);
359 }
360
361 // Initialize digital rain effect if enabled
362 if (GET_OPTION(matrix_rain)) {
363 // Get terminal dimensions for grid size
364 unsigned short int width_us = terminal_get_effective_width(GET_OPTION(width), 80);
365 unsigned short int height_us = terminal_get_effective_height(GET_OPTION(height), 24);
366
367 int width = (int)width_us;
368 int height = (int)height_us;
369
371 if (ctx->digital_rain) {
372 // Set color from color filter if active
373 color_filter_t filter = GET_OPTION(color_filter);
375 log_info("Digital rain effect enabled: %dx%d grid", width, height);
376 } else {
377 log_warn("Failed to initialize digital rain effect");
378 }
379 ctx->last_frame_time_ns = time_get_ns();
380 }
381
384 return NULL;
385 }
386
387 display_sync_entries(ctx, true);
388
389 // Initialize render-file unless media setup needs to determine its timing first.
390 const char *render_file_opt = GET_OPTION(render_file);
391 log_debug("render-file opt='%s' (len=%zu), deferred=%d", render_file_opt ? render_file_opt : "(null)",
392 render_file_opt ? strlen(render_file_opt) : 0, config->defer_render_file);
393 if (!config->defer_render_file) {
395 if (render_err != ASCIICHAT_OK) {
396 log_error("render-file: Initialization failed %s", asciichat_error_format(NULL, render_err));
398 bool has_error = HAS_ERRNO(&error);
400 SET_ERRNO(render_err, "%s", has_error ? error.context_message : "Render-file initialization failed");
401 return NULL;
402 }
403 }
404
405 ctx->initialized = true;
406 if (ctx->has_tty && !ctx->snapshot_mode) {
407 const char initial[] = "\033[H";
409 (terminal_size_t){.cols = UI_MEDIA_MIN_COLS, .rows = UI_MEDIA_MIN_ROWS},
410 initial, sizeof(initial) - 1);
411 }
412
413 return ctx;
414}
const char * asciichat_error_format(const char *message, asciichat_error_t code)
asciichat_error_t audio_visualization_fft_init(void)
Definition fft.c:61
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
void ansi_fast_init_256color(void)
Definition ansi.c:232
void ansi_fast_init(void)
Definition ansi.c:227
void ansi_fast_init_16color(void)
Definition ansi.c:315
@ ERROR_USAGE
Definition error_codes.h:48
void log_set_force_stderr(bool enabled)
Force all terminal log output to stderr.
Definition log.c:767
#define options_terminal_settings()
Definition options.h:1296
@ UTF8_SETTING_AUTO
Smart detection (default): UTF-8 if terminal supports it.
Definition options.h:264
@ UTF8_SETTING_FALSE
Force UTF-8 OFF: disable UTF-8 support.
Definition options.h:268
int initialize_client_palette(palette_type_t palette_type, const char *custom_chars, char client_palette_chars[ASCII_PALETTE_BUFFER_SIZE], size_t *client_palette_len, char client_luminance_palette[ASCII_LUMINANCE_LEVELS])
Initialize client palette with full configuration.
Definition palette.c:306
int platform_dup(int fd)
utf8_palette_cache_t * get_utf8_palette_cache(const char *ascii_chars)
Get UTF-8 palette cache for a character set.
asciichat_error_t ascii_write_init(int fd, bool reset_terminal)
Initialize ASCII write subsystem.
Definition ascii.c:128
void digital_rain_set_color_from_filter(digital_rain_t *rain, color_filter_t filter)
Set digital rain color from color filter.
digital_rain_t * digital_rain_init(int num_columns, int num_rows)
Initialize digital rain effect context.
asciichat_error_t session_display_init_render_file(session_display_ctx_t *ctx, uint32_t fps)
Perform complete terminal reset.
void * callback_data
Opaque data passed to should_exit_callback.
palette_type_t palette_type
Palette type for ASCII rendering.
void * audio_ctx
Audio context for playback (borrowed, not owned).
bool defer_render_file
Defer render-file encoder creation until media timing is known.
const char * custom_palette
Custom palette characters (required if palette_type == PALETTE_CUSTOM).
bool enable_audio_playback
Enable audio playback (mirror mode).
session_display_should_exit_fn should_exit_callback
Optional: callback to check if initialization should be cancelled (e.g., shutdown signal).
uint32_t render_fps
Video FPS for render-file encoding (0 = use default/option).
bool snapshot_mode
Enable snapshot mode (single frame capture).
terminal_color_mode_t color_mode
Color mode override (TERM_COLOR_AUTO for auto-detection).
char luminance_palette[ASCII_LUMINANCE_LEVELS]
Luminance-to-character mapping table (256 entries).
bool has_tty
True if we have a valid TTY for interactive output.
tty_info_t tty_info
TTY information (file descriptor, path, ownership).
digital_rain_t * digital_rain
Digital rain effect context (NULL if disabled).
char palette_chars[ASCII_PALETTE_BUFFER_SIZE]
Palette character string for rendering.
uint64_t last_frame_time_ns
Last frame timestamp for digital rain delta time calculation.
bool initialized
Context is fully initialized.
atomic_t first_frame
First frame flag for logging control.
void * audio_ctx
Audio context for playback (borrowed, not owned).
atomic_t output_failed
Stop submitting frames after the output consumer disappears.
terminal_capabilities_t caps
Detected terminal capabilities.
bool audio_playback_enabled
Audio playback is enabled.
palette_type_t palette_type
Configured palette type.
uint32_t render_fps
Video FPS for render-file encoding.
size_t palette_len
Number of characters in palette.
asciichat_error_t ui_controller_present_retained(ui_screen_t screen, int fd, terminal_size_t minimum, const char *data, size_t len)
@ UI_SCREEN_MEDIA

References ansi_fast_init(), ansi_fast_init_16color(), ansi_fast_init_256color(), ASCII_OTEL_SCOPE, ascii_write_init(), asciichat_error_format(), ASCIICHAT_OK, session_display_config_t::audio_ctx, session_display_ctx::audio_ctx, session_display_ctx::audio_playback_enabled, audio_visualization_fft_init(), session_display_config_t::callback_data, session_display_ctx::caps, session_display_config_t::color_mode, asciichat_error_context_t::context_message, session_display_config_t::custom_palette, session_display_config_t::defer_render_file, session_display_ctx::digital_rain, digital_rain_init(), digital_rain_set_color_from_filter(), session_display_config_t::enable_audio_playback, ERROR_USAGE, session_display_ctx::first_frame, GET_OPTION, get_utf8_palette_cache(), HAS_ERRNO, session_display_ctx::has_tty, height, initialize_client_palette(), session_display_ctx::initialized, session_display_ctx::last_frame_time_ns, log_debug, log_error, log_info, log_set_force_stderr(), log_warn, session_display_ctx::luminance_palette, options_terminal_settings, session_display_ctx::output_failed, session_display_ctx::palette_chars, session_display_ctx::palette_len, PALETTE_STANDARD, session_display_config_t::palette_type, session_display_ctx::palette_type, platform_dup(), session_display_config_t::render_fps, session_display_ctx::render_fps, SAFE_CALLOC, session_display_destroy(), session_display_init_render_file(), SET_ERRNO, session_display_config_t::should_exit_callback, session_display_config_t::snapshot_mode, session_display_ctx::snapshot_mode, session_display_ctx::tty_info, ui_controller_present_retained(), UI_SCREEN_MEDIA, UTF8_SETTING_AUTO, UTF8_SETTING_FALSE, and width.

Referenced by display_init().

◆ session_display_cursor_home()

void session_display_cursor_home ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Move cursor to home position (top-left).

Parameters
ctxDisplay context (must not be NULL)

Moves the cursor to the top-left corner (1,1) of the terminal.

Definition at line 1375 of file src/common/session/display.c.

1375 {
1376 if (ctx && !ctx->snapshot_mode)
1378}

References session_display_ctx::snapshot_mode, and ui_controller_redraw().

◆ session_display_destroy()

void session_display_destroy ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Definition at line 431 of file src/common/session/display.c.

431 {
432 if (!ctx) {
433 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL");
434 return;
435 }
436
438
441
442 // Cleanup ASCII rendering if we had a TTY
443 // Don't reset terminal in snapshot mode to preserve the rendered output
444 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
446 }
447
448 // Close the controlling terminal if we opened it
449 if (ctx->tty_info.owns_fd && ctx->tty_info.fd >= 0) {
450 (void)platform_close(ctx->tty_info.fd);
451 ctx->tty_info.fd = -1;
452 ctx->tty_info.owns_fd = false;
453 }
454
455 // Cleanup digital rain effect
456 if (ctx->digital_rain) {
458 ctx->digital_rain = NULL;
459 }
460
464 ctx->resize_workspace = ctx->flip_workspace = ctx->filter_workspace = NULL;
469 ctx->initialized = false;
470
471 // Clear the cached display context in splash state to prevent use-after-free
472 // when worker threads try to access the display after it's been freed
474
475 display_sync_entries(ctx, false);
476 SAFE_FREE(ctx);
477}
asciichat_error_t session_display_finish_render_file(session_display_ctx_t *ctx)
Destroy session display context and free resources.
void splash_clear_display_context(void)
Clear the cached display context to prevent use-after-free.
Definition splash.c:441
void ascii_write_destroy(int fd, bool reset_terminal)
Destroy ASCII write subsystem.
Definition ascii.c:559
void digital_rain_destroy(digital_rain_t *rain)
Destroy digital rain effect context.
void ascii_stabilizer_reset(ascii_stabilizer_t *state)
char * buf
Buffer pointer (allocated, owned by caller, must be freed).
ascii_stabilizer_t recording_stabilizer

References ascii_stabilizer_reset(), session_display_ctx::ascii_workspace, ascii_write_destroy(), outbuf_t::buf, session_display_ctx::digital_rain, digital_rain_destroy(), ERROR_INVALID_PARAM, session_display_ctx::filter_workspace, session_display_ctx::flip_workspace, session_display_ctx::has_tty, image_destroy(), session_display_ctx::initialized, session_display_ctx::recording_stabilizer, session_display_ctx::resize_workspace, SAFE_FREE, session_display_finish_render_file(), SET_ERRNO, session_display_ctx::snapshot_mode, splash_clear_display_context(), session_display_ctx::stabilizer, session_display_ctx::tty_frame_buffer, session_display_ctx::tty_info, ui_controller_remove(), UI_SCREEN_HELP, and UI_SCREEN_MEDIA.

Referenced by session_display_create().

◆ session_display_encode_frame()

asciichat_error_t session_display_encode_frame ( session_display_ctx_t * ctx,
const image_t * image,
uint64_t captured_ns )

#include <src/common/session/display.h>

Encode frame to render-file (FFmpeg only, no terminal output).

Parameters
ctxDisplay context (must not be NULL)
imageRaw image to encode (must not be NULL)

Encodes image via render_file/FFmpeg encoder. No-op if –render-file not set. Used by the encode thread in the threaded pipeline.

Definition at line 1285 of file src/common/session/display.c.

1285 {
1286 ASCII_OTEL_SCOPE(SESSION_DISPLAY_ENCODE_FRAME);
1287 static int call_count = 0;
1288 if (call_count++ < 5) {
1289 log_debug("ctx=%p, image=%p, captured_ns=%llu, ctx->render_file=%p", (void *)ctx, (void *)image,
1290 (unsigned long long)captured_ns, ctx ? (void *)ctx->render_file : NULL);
1291 }
1292
1293 if (!ctx || !ctx->initialized) {
1294 return SET_ERRNO(ERROR_INVALID_PARAM, "Display context is NULL or uninitialized");
1295 }
1296
1297 if (!image) {
1298 return SET_ERRNO(ERROR_INVALID_PARAM, "Image is NULL");
1299 }
1300
1301 // Only encode if render_file is active
1302 if (!ctx->render_file) {
1303 log_warn_every(10 * US_PER_SEC_INT, "render_file is NULL, skipping encode");
1304 return ASCIICHAT_OK;
1305 }
1306
1307 // Convert image to ASCII
1308 uint64_t conversion_started_ns = time_get_ns();
1309 char *ascii = session_display_convert_to_ascii(ctx, image);
1310 uint64_t converted_ns = time_get_ns();
1311 if (!ascii) {
1312 log_warn("Failed to convert image to ASCII");
1313 return SET_ERRNO(ERROR_MEDIA_INIT, "Cannot convert recording frame to ASCII");
1314 }
1315
1316 // Write the same selected visualization used by the terminal to the render-file encoder.
1317 char *visualization_frame = NULL;
1318 const char *render_frame = ascii;
1319 if (GET_OPTION(waveform) || GET_OPTION(fft)) {
1320 visualization_frame = session_display_create_visualization_frame(ctx);
1321 if (visualization_frame)
1322 render_frame = visualization_frame;
1323 }
1324
1325 uint64_t stats_record_start = time_get_ns();
1326 asciichat_error_t fe = render_file_write_frame(ctx->render_file, render_frame, captured_ns);
1327 uint64_t recorded_ns = time_get_ns();
1328 log_debug_every(US_PER_SEC_INT, "RECORD_FRAME: convert_ms=%.2f output_ms=%.2f total_ms=%.2f",
1329 (double)(converted_ns - conversion_started_ns) / NS_PER_MS_INT,
1330 (double)(recorded_ns - converted_ns) / NS_PER_MS_INT,
1331 (double)(recorded_ns - conversion_started_ns) / NS_PER_MS_INT);
1332 stats_duration_record(stats_runtime_scope(), STATS_DURATION_RECORDING, time_get_ns() - stats_record_start);
1333 if (fe == ASCIICHAT_OK)
1335 SAFE_FREE(visualization_frame);
1336 if (fe != ASCIICHAT_OK) {
1337 log_warn_every(5 * US_PER_SEC_INT, "render-file: encode failed (%s)", asciichat_error_format(NULL, fe));
1338 }
1339
1340 SAFE_FREE(ascii);
1341 return fe;
1342}
#define US_PER_SEC_INT
Definition time.h:173
char * session_display_convert_to_ascii(session_display_ctx_t *ctx, const image_t *image)
Convert an image to ASCII art using display context and command-line options.
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)
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:749
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:755
asciichat_error_t render_file_write_frame(render_file_ctx_t *ctx, const char *frame, uint64_t captured_ns)
Definition renderer.c:348
stats_scope_t * stats_runtime_scope(void)
Definition runtime.c:80

References ASCII_OTEL_SCOPE, asciichat_error_format(), ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_MEDIA_INIT, GET_OPTION, session_display_ctx::initialized, log_debug, log_debug_every, log_warn, log_warn_every, NS_PER_MS_INT, session_display_ctx::render_file, render_file_write_frame(), SAFE_FREE, session_display_convert_to_ascii(), SET_ERRNO, stats_counter_add(), STATS_COUNTER_RECORDING_FRAMES, stats_duration_record(), STATS_DURATION_RECORDING, stats_runtime_scope(), and US_PER_SEC_INT.

◆ session_display_finish_render_file()

asciichat_error_t session_display_finish_render_file ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Destroy session display context and free resources.

Parameters
ctxDisplay context to destroy (can be NULL)

Cleans up the display context, restores terminal state, and releases all resources. Safe to call with NULL.

Definition at line 423 of file src/common/session/display.c.

423 {
424 if (!ctx || !ctx->render_file)
425 return ASCIICHAT_OK;
427 ctx->render_file = NULL;
428 return err;
429}
asciichat_error_t render_file_destroy(render_file_ctx_t *ctx)
Definition renderer.c:375

References ASCIICHAT_OK, session_display_ctx::render_file, and render_file_destroy().

Referenced by session_display_destroy().

◆ session_display_get_caps()

const terminal_capabilities_t * session_display_get_caps ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Get detected terminal capabilities.

Parameters
ctxDisplay context (must not be NULL)
Returns
Pointer to terminal capabilities structure

Returns the detected terminal capabilities including color level, UTF-8 support, and render mode preferences.

Note
Returns NULL if context is NULL or not initialized.

Definition at line 501 of file src/common/session/display.c.

501 {
502 if (!ctx || !ctx->initialized) {
503 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
504 return NULL;
505 }
506 return &ctx->caps;
507}

References session_display_ctx::caps, ERROR_INVALID_PARAM, session_display_ctx::initialized, and SET_ERRNO.

◆ session_display_get_global_context()

session_display_ctx_t * session_display_get_global_context ( void )

#include <src/common/session/display.h>

Get the global display context (used for signal handlers and special cleanup).

Returns
Pointer to global display context, or NULL if not set

Retrieves the display context that was set via session_display_set_global_context(). Signal handlers and special modes (like discovery participant) use this to access the display without having a direct pointer.

Definition at line 1465 of file src/common/session/display.c.

1465 {
1466 return get_current_display_ctx();
1467}

◆ session_display_get_luminance_palette()

const char * session_display_get_luminance_palette ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Get the luminance mapping palette.

Parameters
ctxDisplay context (must not be NULL)
Returns
Pointer to 256-entry luminance mapping array

Returns the luminance mapping palette for direct brightness-to-character lookup during rendering.

Definition at line 525 of file src/common/session/display.c.

525 {
526 if (!ctx || !ctx->initialized) {
527 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
528 return NULL;
529 }
530 return ctx->luminance_palette;
531}

References ERROR_INVALID_PARAM, session_display_ctx::initialized, session_display_ctx::luminance_palette, and SET_ERRNO.

◆ session_display_get_palette_chars()

const char * session_display_get_palette_chars ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Get the palette characters string.

Parameters
ctxDisplay context (must not be NULL)
Returns
Pointer to palette character string

Returns the initialized palette character string used for luminance-to-character mapping.

Definition at line 509 of file src/common/session/display.c.

509 {
510 if (!ctx || !ctx->initialized) {
511 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
512 return NULL;
513 }
514 return ctx->palette_chars;
515}

References ERROR_INVALID_PARAM, session_display_ctx::initialized, session_display_ctx::palette_chars, and SET_ERRNO.

◆ session_display_get_palette_len()

size_t session_display_get_palette_len ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Get the palette character count.

Parameters
ctxDisplay context (must not be NULL)
Returns
Number of characters in the palette

Definition at line 517 of file src/common/session/display.c.

517 {
518 if (!ctx || !ctx->initialized) {
519 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
520 return 0;
521 }
522 return ctx->palette_len;
523}

References ERROR_INVALID_PARAM, session_display_ctx::initialized, session_display_ctx::palette_len, and SET_ERRNO.

◆ session_display_get_render_fps()

uint32_t session_display_get_render_fps ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Get the render FPS configured for file output.

Parameters
ctxDisplay context (can be NULL)
Returns
Render FPS value, or 0 if ctx is NULL

Definition at line 550 of file src/common/session/display.c.

550 {
551 if (!ctx)
552 return 0;
553 return ctx->render_fps;
554}

References session_display_ctx::render_fps.

Referenced by session_pipeline_create().

◆ session_display_get_stdin_reader()

void * session_display_get_stdin_reader ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Get the stdin frame reader for ASCII-to-video rendering.

Parameters
ctxDisplay context (must not be NULL)
Returns
Pointer to stdin reader, or NULL if not configured

Definition at line 541 of file src/common/session/display.c.

541 {
542 if (!ctx || !ctx->initialized) {
543 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
544 return NULL;
545 }
546
547 return ctx->stdin_reader;
548}
terminal_fd_reader_t * stdin_reader
Stdin frame reader for ASCII-to-video rendering (borrowed ref, not owned).

References ERROR_INVALID_PARAM, session_display_ctx::initialized, SET_ERRNO, and session_display_ctx::stdin_reader.

◆ session_display_get_tty_fd()

int session_display_get_tty_fd ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Get the TTY file descriptor.

Parameters
ctxDisplay context (must not be NULL)
Returns
TTY file descriptor, or -1 if no TTY available

Definition at line 533 of file src/common/session/display.c.

533 {
534 if (!ctx || !ctx->initialized) {
535 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
536 return -1;
537 }
538 return ctx->tty_info.fd;
539}

References ERROR_INVALID_PARAM, session_display_ctx::initialized, SET_ERRNO, and session_display_ctx::tty_info.

◆ session_display_has_audio_playback()

bool session_display_has_audio_playback ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Check if display has audio playback configured.

Parameters
ctxDisplay context (must not be NULL)
Returns
true if audio playback is available, false otherwise

Definition at line 1380 of file src/common/session/display.c.

1380 {
1381 if (!ctx || !ctx->initialized) {
1382 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL or uninitialized");
1383 return false;
1384 }
1385 return ctx->audio_playback_enabled;
1386}

References session_display_ctx::audio_playback_enabled, ERROR_INVALID_PARAM, session_display_ctx::initialized, and SET_ERRNO.

◆ session_display_has_first_frame()

bool session_display_has_first_frame ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Check if the display has rendered its first frame.

Parameters
ctxDisplay context (can be NULL)
Returns
true if first frame has been rendered, false if not yet rendered or ctx is NULL

Definition at line 565 of file src/common/session/display.c.

565 {
566 if (!ctx) {
567 return false;
568 }
569 // Return true if first_frame flag is false (meaning first frame has been rendered)
570 return !atomic_load_bool(&ctx->first_frame);
571}

References session_display_ctx::first_frame.

◆ session_display_has_render_file()

bool session_display_has_render_file ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Check if display has render-file configured.

Parameters
ctxDisplay context (can be NULL)
Returns
true if render-file is configured, false otherwise

Definition at line 581 of file src/common/session/display.c.

581 {
582 if (!ctx) {
583 return false;
584 }
585 return ctx->render_file != NULL;
586}

References session_display_ctx::render_file.

Referenced by session_pipeline_create().

◆ session_display_has_tty()

bool session_display_has_tty ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Check if display has a TTY (terminal) available.

Parameters
ctxDisplay context (must not be NULL)
Returns
true if TTY is available, false otherwise

Returns whether the display has detected and opened a TTY. When no TTY is available, output goes to stdout.

Definition at line 493 of file src/common/session/display.c.

493 {
494 if (!ctx || !ctx->initialized) {
495 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
496 return false;
497 }
498 return ctx->has_tty;
499}

References ERROR_INVALID_PARAM, session_display_ctx::has_tty, session_display_ctx::initialized, and SET_ERRNO.

Referenced by display_has_tty().

◆ session_display_render_frame()

void session_display_render_frame ( session_display_ctx_t * ctx,
const char * frame_data )

#include <src/common/session/display.h>

Render an ASCII frame to the terminal.

Parameters
ctxDisplay context (must not be NULL)
frame_dataASCII frame data to render (must not be NULL)
is_finaltrue if this is the final frame (for snapshot mode)

Renders the ASCII frame to the terminal. Handles cursor positioning, RLE expansion if needed, and snapshot mode behavior.

In snapshot mode, renders all frames during the snapshot window. (Legacy function; new code should use split functions for threading.)

Definition at line 899 of file src/common/session/display.c.

899 {
900 // Legacy function: shim that calls both new split functions for backward compatibility
901 // This maintains API compatibility with existing code while delegating to specialized functions
902
903 if (!ctx || !frame_data) {
904 SET_ERRNO(ERROR_INVALID_PARAM, "Display context or frame data is NULL");
905 return;
906 }
907
908 // Write the selected visualization to the render file as well as the terminal.
909 char *visualization_frame = NULL;
910 const char *render_frame = frame_data;
911 if (GET_OPTION(waveform) || GET_OPTION(fft)) {
912 visualization_frame = session_display_create_visualization_frame(ctx);
913 if (visualization_frame)
914 render_frame = visualization_frame;
915 }
916
917 // Write ASCII to terminal (main thread output path)
918 if (ctx && ctx->render_file) {
920 asciichat_error_t err = render_file_write_frame(ctx->render_file, render_frame, time_get_ns());
921 if (err != ASCIICHAT_OK)
922 log_warn_every(5 * US_PER_SEC_INT, "Recording received frame failed: %s", asciichat_error_format(NULL, err));
923 }
924 SAFE_FREE(visualization_frame);
925 session_display_write_ascii(ctx, frame_data);
926}
void session_display_write_ascii(session_display_ctx_t *ctx, const char *ascii)
Write ASCII frame to terminal only (no encoding).
void render_file_set_live_timing(render_file_ctx_t *ctx)
Definition renderer.c:266

References asciichat_error_format(), ASCIICHAT_OK, ERROR_INVALID_PARAM, GET_OPTION, log_warn_every, session_display_ctx::render_file, render_file_set_live_timing(), render_file_write_frame(), SAFE_FREE, session_display_write_ascii(), SET_ERRNO, and US_PER_SEC_INT.

Referenced by display_render_frame(), and session_render_loop().

◆ session_display_reset()

void session_display_reset ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Reset terminal to default state.

Parameters
ctxDisplay context (must not be NULL)

Resets terminal attributes (colors, cursor visibility, etc.) to defaults. Useful for cleanup or error recovery.

Definition at line 1365 of file src/common/session/display.c.

1365 {
1366 if (ctx && !ctx->snapshot_mode)
1368}

References session_display_ctx::snapshot_mode, and ui_controller_redraw().

Referenced by display_full_reset().

◆ session_display_reset_first_frame()

void session_display_reset_first_frame ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Reset the first frame flag to allow splash screen to show again on next render.

Parameters
ctxDisplay context (must not be NULL)

Used during reconnection attempts to re-enable splash screen display. After calling this, the next call to session_display_render_frame() will trigger splash cleanup logic, allowing the splash to be shown again.

Definition at line 573 of file src/common/session/display.c.

573 {
574 if (!ctx) {
575 return;
576 }
577 // Reset first_frame flag to true so splash screen can be shown again on next render
578 atomic_store_bool(&ctx->first_frame, true);
579}

References session_display_ctx::first_frame.

◆ session_display_set_global_context()

void session_display_set_global_context ( session_display_ctx_t * ctx)

#include <include/ascii-chat/tui/keyboard_help.h>

Set the global display context (for signal handlers like Ctrl+C).

Parameters
ctxDisplay context (NULL to clear)

Called by the render loop to register the current display context so that signal handlers can access it. This enables Ctrl+C to close the help screen instead of quitting when help is active.

Note
Must be called from the render loop startup/shutdown

Definition at line 1460 of file src/common/session/display.c.

1460 {
1461 atomic_ptr_store(&g_current_display_ctx, (void *)ctx);
1462}

Referenced by session_display_set_global_context_public(), and session_render_loop().

◆ session_display_set_global_context_public()

void session_display_set_global_context_public ( session_display_ctx_t * ctx)

#include <src/common/session/display.h>

Set the global display context (public setter for discovery cleanup).

Parameters
ctxDisplay context to set, or NULL to clear

Sets the global display context. Signal handlers and special modes use this to track the active display context. Used by discovery participant mode to clear the global context after destroying the display early to prevent use-after-free in cleanup code.

Definition at line 1470 of file src/common/session/display.c.

1470 {
1472}
void session_display_set_global_context(session_display_ctx_t *ctx)
Set the global display context (for signal handlers like Ctrl+C).

References session_display_set_global_context().

◆ session_display_set_render_fps()

void session_display_set_render_fps ( session_display_ctx_t * ctx,
uint32_t fps )

#include <src/common/session/display.h>

Set the render FPS for file output encoding.

Parameters
ctxDisplay context
fpsTarget FPS for video encoding (detected from input media)

Definition at line 556 of file src/common/session/display.c.

556 {
557 if (!ctx)
558 return;
559 ctx->render_fps = fps;
560 if (fps > 0) {
561 log_debug("Updated render FPS to %u (for correct video duration)", fps);
562 }
563}

References log_debug, and session_display_ctx::render_fps.

◆ session_display_set_snapshot_actual_duration()

void session_display_set_snapshot_actual_duration ( session_display_ctx_t * ctx,
double actual_duration_sec )

#include <src/common/session/display.h>

Set the actual wall-clock duration for snapshot mode frame timing.

Parameters
ctxDisplay context (must not be NULL)
actual_duration_secActual elapsed time in seconds

In snapshot mode, this function provides the actual wall-clock duration of frame capture, allowing the encoder to calculate correct frame durations so the output video spans exactly the specified duration.

Definition at line 1511 of file src/common/session/display.c.

1511 {
1512 log_debug("duration=%.3f, ctx=%p, render_file=%p", actual_duration_sec, (void *)ctx,
1513 ctx ? (void *)ctx->render_file : NULL);
1514
1515 if (!ctx) {
1516 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL");
1517 return;
1518 }
1519
1520 if (!ctx->render_file) {
1521 // Terminal snapshots have no render-file encoder; their duration is
1522 // governed by the capture loop and requires no additional action.
1523 return;
1524 }
1525
1526 log_debug(" -> Passing duration %.3f to render_file encoder", actual_duration_sec);
1527 render_file_set_snapshot_actual_duration(ctx->render_file, actual_duration_sec);
1528}
void render_file_set_snapshot_actual_duration(render_file_ctx_t *ctx, double actual_duration_sec)
Definition renderer.c:369

References ERROR_INVALID_PARAM, log_debug, session_display_ctx::render_file, render_file_set_snapshot_actual_duration(), and SET_ERRNO.

Referenced by session_render_loop().

◆ session_display_set_stdin_reader()

void session_display_set_stdin_reader ( session_display_ctx_t * ctx,
void * reader )

#include <src/common/session/display.h>

Set stdin frame reader for ASCII-to-video rendering.

Parameters
ctxDisplay context (must not be NULL)
readerStdin frame reader (borrowed reference, not owned)

Only used when reading ASCII frames from stdin for rendering.

Definition at line 479 of file src/common/session/display.c.

479 {
480 if (!ctx) {
481 SET_ERRNO(ERROR_INVALID_PARAM, "Display context is NULL");
482 return;
483 }
484
485 ctx->stdin_reader = (terminal_fd_reader_t *)reader;
486 log_debug("stdin_reader set");
487}
struct terminal_fd_reader_s terminal_fd_reader_t

References ERROR_INVALID_PARAM, log_debug, SET_ERRNO, and session_display_ctx::stdin_reader.

◆ session_display_write_ascii()

void session_display_write_ascii ( session_display_ctx_t * ctx,
const char * frame_data )

#include <src/common/session/display.h>

Write ASCII frame to terminal only (no encoding).

Parameters
ctxDisplay context (must not be NULL)
frame_dataASCII frame data (must not be NULL)

Writes ANSI string to terminal/stdout without encoding to file. Used by the main thread in the threaded pipeline.

Definition at line 1530 of file src/common/session/display.c.

1530 {
1531 session_display_write_ascii_frame(ctx, ascii, true);
1532}
void session_display_write_ascii_frame(session_display_ctx_t *ctx, const char *ascii, bool fresh)

References session_display_write_ascii_frame().

Referenced by session_display_render_frame().

◆ session_display_write_audio()

asciichat_error_t session_display_write_audio ( session_display_ctx_t * ctx,
const float * buffer,
size_t num_samples )

#include <src/common/session/display.h>

Write audio samples to playback buffer.

Parameters
ctxDisplay context (must not be NULL)
bufferAudio samples to write (must not be NULL)
num_samplesNumber of samples to write
Returns
ASCIICHAT_OK on success, error code on failure

Writes audio samples to the playback ring buffer for playback through speakers. Used in mirror mode to play file audio or other audio sources.

Definition at line 1388 of file src/common/session/display.c.

1388 {
1389 ASCII_OTEL_SCOPE(SESSION_DISPLAY_WRITE_AUDIO);
1390 if (!ctx || !ctx->initialized || !buffer || num_samples == 0) {
1391 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, buffer=%p, num_samples=%zu", ctx, buffer,
1392 num_samples);
1393 }
1394
1395 if (!ctx->audio_playback_enabled || !ctx->audio_ctx) {
1396 // Audio not enabled, but not an error - just skip silently
1397 return ASCIICHAT_OK;
1398 }
1399
1400 // For mirror mode with local files: write samples directly without jitter buffering
1401 // The jitter buffer is designed for network scenarios with irregular packet arrivals
1402 // For local playback, we just want raw samples flowing to the speakers
1403 audio_context_t *audio_ctx = (audio_context_t *)ctx->audio_ctx;
1404 if (audio_ctx && audio_ctx->playback_buffer) {
1405 audio_ring_buffer_t *rb = audio_ctx->playback_buffer;
1406
1407 // Simple direct write: append samples to ring buffer without network-oriented complexity
1408 uint32_t write_idx = (uint32_t)atomic_load_u64(&rb->write_index);
1409 uint32_t read_idx = (uint32_t)atomic_load_u64(&rb->read_index);
1410
1411 // Calculate available space in ring buffer
1412 uint32_t available = (read_idx - write_idx - 1) & (AUDIO_RING_BUFFER_SIZE - 1);
1413
1414 if ((uint32_t)num_samples > available) {
1415 // Buffer full - just skip this write to avoid distortion from overwriting old audio
1416 return ASCIICHAT_OK;
1417 }
1418
1419 // Direct memcpy to ring buffer, handling wrap-around
1420 uint32_t space_before_wrap = AUDIO_RING_BUFFER_SIZE - write_idx;
1421 if ((uint32_t)num_samples <= space_before_wrap) {
1422 memcpy(&rb->data[write_idx], buffer, num_samples * sizeof(float));
1423 } else {
1424 // Split write: first part to end of buffer, second part wraps to beginning
1425 uint32_t first_part = space_before_wrap;
1426 uint32_t second_part = num_samples - first_part;
1427 memcpy(&rb->data[write_idx], buffer, first_part * sizeof(float));
1428 memcpy(&rb->data[0], &buffer[first_part], second_part * sizeof(float));
1429 }
1430
1431 // Update write index atomically
1432 atomic_store_u64(&rb->write_index, (write_idx + num_samples) % AUDIO_RING_BUFFER_SIZE);
1433
1434 return ASCIICHAT_OK;
1435 }
1436
1437 return ASCIICHAT_OK;
1438}
struct audio_ring_buffer audio_ring_buffer_t
Audio ring buffer for real-time audio streaming.
#define AUDIO_RING_BUFFER_SIZE
Audio ring buffer size in samples (192000 samples = 4 seconds @ 48kHz).
Definition ringbuffer.h:131
When code needs to work with audio device info, it keeps index, max_input_channels,...
atomic_t read_index
Read index (consumer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:215
atomic_t write_index
Write index (producer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:213
float data[192000]
Audio sample data buffer.
Definition ringbuffer.h:211

References ASCII_OTEL_SCOPE, ASCIICHAT_OK, session_display_ctx::audio_ctx, session_display_ctx::audio_playback_enabled, AUDIO_RING_BUFFER_SIZE, audio_ring_buffer::data, ERROR_INVALID_PARAM, session_display_ctx::initialized, audio_context_t::playback_buffer, audio_ring_buffer::read_index, SET_ERRNO, and audio_ring_buffer::write_index.

◆ session_display_write_raw()

void session_display_write_raw ( session_display_ctx_t * ctx,
const char * data,
size_t len )

#include <src/common/session/display.h>

Render raw bytes to the terminal without frame processing.

Parameters
ctxDisplay context (must not be NULL)
dataRaw byte data to write (must not be NULL)
lenLength of data in bytes

Directly writes raw bytes to the terminal without any frame processing. Useful for RLE-expanded frames or pre-formatted output.

Definition at line 1344 of file src/common/session/display.c.

1344 {
1345 ASCII_OTEL_SCOPE(SESSION_DISPLAY_WRITE_RAW);
1346 if (!ctx || !ctx->initialized || !data || len == 0) {
1347 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, data=%p, len=%zu", ctx, data, len);
1348 return;
1349 }
1350
1351 int fd = -1;
1352 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
1353 fd = ctx->tty_info.fd;
1354 } else {
1355 fd = STDOUT_FILENO;
1356 }
1357
1358 // Write all data with automatic retry on transient errors
1359 (void)ui_controller_write(fd, data, len);
1360
1361 // Flush immediately after write to TTY to ensure data is sent
1362 (void)terminal_flush(fd);
1363}
asciichat_error_t ui_controller_write(int fd, const char *data, size_t len)

References ASCII_OTEL_SCOPE, ERROR_INVALID_PARAM, session_display_ctx::has_tty, session_display_ctx::initialized, SET_ERRNO, session_display_ctx::tty_info, and ui_controller_write().

◆ session_handle_keyboard_input()

void session_handle_keyboard_input ( session_capture_ctx_t * capture,
session_display_ctx_t * display,
keyboard_key_t key )

#include <src/common/session/keyboard_handler.h>

Handle keyboard input in a session.

Parameters
captureCapture context (may be NULL for client-only mode)
displayDisplay context (may be NULL if not needed)
keyKeyboard key code from keyboard_key_t enumeration

Processes keyboard input and performs appropriate session actions. Supports both mirror mode (local capture) and client mode (network rendering).

Keyboard Controls:

  • '?' key: Toggle help screen on/off (all modes)
  • Left Arrow: Seek -30 seconds (file/URL sources only)
  • Right Arrow: Seek +30 seconds (file/URL sources only)
  • Up Arrow: Increase volume +10% (all modes with audio)
  • Down Arrow: Decrease volume -10% (all modes with audio)
  • Spacebar: Play/pause toggle (file/URL sources only)
  • 'c' key: Cycle through color modes (mono โ†’ 16 โ†’ 256 โ†’ truecolor)
  • 'm' key: Toggle mute (remembers previous volume for unmute)
  • 'x' key: Flip webcam horizontally (mirroring)
  • 'y' key: Flip webcam vertically

Source-specific behavior:

  • Help screen: Works in all modes (toggles with '?')
  • Seek/pause controls: Only work with file and URL sources (not webcam)
  • Volume/mute: Work with any audio-capable source
  • Color cycling: Works with all rendering modes
  • Flip: Works with all capture sources (webcam, media files, etc.)

Audio volume:

  • Volume range: [0.0, 1.0] (0% = silent, 100% = maximum/normal)
  • Default volume: 1.0 (100%)
  • Mute state: Remembers the previous non-zero volume when toggled back on
  • Volume and mute controls show a right-side bar for 1.5 seconds after the last press
  • Feedback works while paused; pausing alone does not show the bar
  • Unrecognized keys are silently ignored (no error returned)

Thread safety:

  • Safe to call from render thread (video/audio threads)
  • Safe to call concurrently from multiple render threads
  • Uses RCU-protected options for atomic updates
  • No explicit locking required (option system handles synchronization)
  • Does not block waiting for I/O or locks

Error handling:

  • Always succeeds (void return)
  • Invalid or unrecognized keys are silently ignored
  • If capture is NULL (client-only mode), seek/pause requests are silently ignored
  • If display is NULL (non-TTY mode), help screen toggle is silently ignored
  • If audio system is unavailable, volume changes are silently ignored
Parameters
captureNULL for client-only mode (network rendering), non-NULL for local capture
displayDisplay context for help screen toggle (may be NULL)
keyValid key code from keyboard_key_t enum (0-127 for ASCII, special key codes)
Note
Safe to call without prior keyboard_init() (silently handles invalid keys)
In client-only mode, capture is NULL (seek/pause unavailable, other controls work)
Volume adjustments are immediate without audible artifacts
Mute persists across multiple mute toggles with volume memory

Definition at line 214 of file keyboard_handler.c.

214 {
215 // Panic-exit bindings are global: they must bypass every modal TUI, prompt,
216 // log search, and media shortcut so they remain available during a hang.
217 if (key == 'Q' || key == KEY_SHIFT_ESCAPE) {
219 }
220
221 bool text_entry = log_search_is_entering();
222 if (!text_entry && (key == '<' || key == '>')) {
223 session_capture_skip_media(capture, key == '>' ? 1 : -1);
224 return;
225 }
226 if (!text_entry && key == KEY_ESCAPE && session_capture_cancel_open(capture))
227 return;
228 if (!text_entry && (key == KEY_MEDIA_SUBMIT || key == KEY_PASTE || tui_shortcut_action(key) == TUI_SHORTCUT_MEDIA ||
229 key == 'v' || key == 'V')) {
232 return;
233 }
237 if (key == KEY_MEDIA_SUBMIT)
238 result = ui_input_media_take(input, sizeof(input));
239 else if (key == KEY_PASTE)
240 result = ui_input_take_paste(input, sizeof(input));
241 else if (key == 'v' || key == 'V')
242 result = platform_clipboard_read(input, sizeof(input));
243 if (result == ASCIICHAT_OK) {
244 if (key != KEY_MEDIA_SUBMIT && input[0]) {
245 media_playlist_t list;
246 result = media_playlist_parse(input, &list);
247 if (result == ASCIICHAT_OK)
248 result = media_playlist_format(&list, normalized, sizeof(normalized));
249 if (result == ASCIICHAT_OK)
250 memcpy(input, normalized, strlen(normalized) + 1);
251 }
252 if (result == ASCIICHAT_OK && key != KEY_MEDIA_SUBMIT)
253 result = ui_input_media_begin(input);
254 if (result == ASCIICHAT_OK && key == KEY_MEDIA_SUBMIT)
255 result = session_capture_open_playlist(capture, input);
256 }
257 if (key == KEY_MEDIA_SUBMIT)
259 if (result != ASCIICHAT_OK) {
261 if (HAS_ERRNO(&error))
262 NOTICE(WARNING, "MEDIA INPUT", "%s", error.context_message);
263 CLEAR_ERRNO();
264 }
265 return;
266 }
268 handle_keyboard_input(capture, display, key);
270}
asciichat_error_t platform_clipboard_read(char *output, size_t capacity)
void session_capture_skip_media(session_capture_ctx_t *ctx, int direction)
asciichat_error_t session_capture_open_playlist(session_capture_ctx_t *ctx, const char *text)
bool session_capture_cancel_open(session_capture_ctx_t *ctx)
void platform_force_exit(int exit_code)
Forcefully terminate the process immediately without cleanup.
@ KEY_SHIFT_ESCAPE
Shift+Escape (immediate exit).
Definition keyboard.h:73
@ KEY_ESCAPE
Escape key (ESC).
Definition keyboard.h:55
@ KEY_MEDIA_SUBMIT
Nonblocking media prompt submitted.
Definition keyboard.h:68
@ KEY_PASTE
Definition keyboard.h:67
#define SESSION_KEYBOARD_HANDLER_INPUT_BUFFER_SIZE
#define SESSION_KEYBOARD_HANDLER_NORMALIZED_BUFFER_SIZE
asciichat_error_t media_playlist_format(const media_playlist_t *list, char *text, size_t capacity)
Definition playlist.c:89
asciichat_error_t media_playlist_parse(const char *text, media_playlist_t *list)
Definition playlist.c:32
void ui_prompt_remove(void)
Definition prompt.c:307
bool log_search_is_entering(void)
Check if currently in input mode (typing pattern).
Definition search.c:408
tui_shortcut_t tui_shortcut_action(keyboard_key_t key)
Definition shortcuts.c:30
@ TUI_SHORTCUT_MEDIA
Definition shortcuts.h:12
asciichat_error_t ui_input_media_take(char *output, size_t capacity)
Definition tui/input.c:789
asciichat_error_t ui_input_take_paste(char *buffer, size_t capacity)
Definition tui/input.c:266
asciichat_error_t ui_input_media_begin(const char *text)
Definition tui/input.c:773

References ASCIICHAT_OK, CLEAR_ERRNO, asciichat_error_context_t::context_message, HAS_ERRNO, KEY_ESCAPE, KEY_MEDIA_SUBMIT, KEY_PASTE, KEY_SHIFT_ESCAPE, log_search_is_entering(), media_playlist_format(), media_playlist_parse(), NOTICE, platform_clipboard_read(), platform_force_exit(), session_capture_cancel_open(), session_capture_lock_source(), session_capture_open_playlist(), session_capture_skip_media(), session_capture_unlock_source(), SESSION_KEYBOARD_HANDLER_INPUT_BUFFER_SIZE, SESSION_KEYBOARD_HANDLER_NORMALIZED_BUFFER_SIZE, tui_shortcut_action(), TUI_SHORTCUT_MEDIA, ui_controller_interactive_screen(), ui_input_media_begin(), ui_input_media_take(), ui_input_take_paste(), ui_prompt_remove(), and UI_SCREEN_PROMPT.

Referenced by display_poll_keyboard().

◆ session_host_add_memory_participant()

uint32_t session_host_add_memory_participant ( session_host_t * host)

#include <src/common/session/host.h>

Add a memory participant (host's own media).

Parameters
hostHost handle (must not be NULL)
Returns
Assigned participant ID on success, 0 on failure

Registers a memory participant for the host's own webcam/audio. This allows the host to participate in the session without network loopback. Media is injected directly into the mixer via session_host_inject_frame().

Note
on_client_join callback is invoked on successful registration.
Only one memory participant per host is supported.

Definition at line 1213 of file host.c.

1213 {
1214 if (!host || !host->initialized) {
1215 return 0;
1216 }
1217
1218 mutex_lock(&host->clients_mutex);
1219
1220 // Check if we have room
1221 if (host->client_count >= host->max_clients) {
1222 mutex_unlock(&host->clients_mutex);
1223 SET_ERRNO(ERROR_SESSION_FULL, "Maximum clients reached");
1224 return 0;
1225 }
1226
1227 // Check if memory participant already exists (only one allowed)
1228 for (int i = 0; i < host->max_clients; i++) {
1229 if (host->clients[i].active && host->clients[i].participant_type == PARTICIPANT_TYPE_MEMORY) {
1230 mutex_unlock(&host->clients_mutex);
1231 SET_ERRNO(ERROR_INVALID_PARAM, "Memory participant already exists");
1232 return 0;
1233 }
1234 }
1235
1236 // Find empty slot
1237 for (int i = 0; i < host->max_clients; i++) {
1238 if (!host->clients[i].active) {
1239 host->clients[i].participant_type = PARTICIPANT_TYPE_MEMORY;
1240 host->clients[i].client_id = host->next_client_id++;
1241 SAFE_STRNCPY(host->clients[i].ip_address, "memory", sizeof(host->clients[i].ip_address));
1242 host->clients[i].port = 0;
1243 host->clients[i].active = true;
1244 host->clients[i].video_active = true; // Memory participants always have active video/audio
1245 host->clients[i].audio_active = true;
1246 host->clients[i].connected_at = (uint64_t)time(NULL);
1247 host->clients[i].transport = NULL;
1248 host->clients[i].transport_ownership = ACIP_TRANSPORT_BORROWED;
1249
1250 // Allocate media buffers (same as network clients)
1251 host->clients[i].incoming_video = image_new(480, 270);
1252 host->clients[i].incoming_audio =
1254
1255 if (!host->clients[i].incoming_video || !host->clients[i].incoming_audio) {
1256 if (host->clients[i].incoming_video) {
1257 image_destroy(host->clients[i].incoming_video);
1258 host->clients[i].incoming_video = NULL;
1259 }
1260 if (host->clients[i].incoming_audio) {
1261 ringbuffer_destroy(host->clients[i].incoming_audio);
1262 host->clients[i].incoming_audio = NULL;
1263 }
1264 host->clients[i].active = false;
1265 mutex_unlock(&host->clients_mutex);
1266 SET_ERRNO(ERROR_MEMORY, "Failed to allocate media buffers for memory participant");
1267 return 0;
1268 }
1269
1270 uint32_t participant_id = host->clients[i].client_id;
1271 host->client_count++;
1272
1273 mutex_unlock(&host->clients_mutex);
1274
1275 log_info("Added memory participant with ID %u", participant_id);
1276
1277 // Invoke callback
1278 if (host->callbacks.on_client_join) {
1279 host->callbacks.on_client_join(host, participant_id, host->user_data);
1280 }
1281
1282 return participant_id;
1283 }
1284 }
1285
1286 mutex_unlock(&host->clients_mutex);
1287 return 0;
1288}
#define OPUS_FRAME_SAMPLES
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
@ ERROR_SESSION_FULL
Definition error_codes.h:82
ringbuffer_t * ringbuffer_create(size_t element_size, size_t capacity)
Create a new ring buffer.
Definition ringbuffer.c:34
void ringbuffer_destroy(ringbuffer_t *rb)
Destroy a ring buffer and free its memory.
Definition ringbuffer.c:61
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
#define HOST_AUDIO_BUFFER_FRAMES
Definition host.c:5
uint8_t participant_id[SESSION_ID_SIZE]

References ACIP_TRANSPORT_BORROWED, AUDIO_CHANNELS, ERROR_INVALID_PARAM, ERROR_MEMORY, ERROR_SESSION_FULL, HOST_AUDIO_BUFFER_FRAMES, image_destroy(), image_new(), log_info, OPUS_FRAME_SAMPLES, participant_id, PARTICIPANT_TYPE_MEMORY, ringbuffer_create(), ringbuffer_destroy(), SAFE_STRNCPY, and SET_ERRNO.

◆ session_host_broadcast_frame()

asciichat_error_t session_host_broadcast_frame ( session_host_t * host,
const char * frame )

#include <src/common/session/host.h>

Broadcast ASCII frame to all clients.

Parameters
hostHost handle (must not be NULL)
frameASCII frame data (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Sends an ASCII frame to all connected clients.

Definition at line 1489 of file host.c.

1489 {
1490 if (!host || !host->initialized || !frame) {
1491 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid parameter");
1492 }
1493
1494 if (!host->running) {
1495 return SET_ERRNO(ERROR_INVALID_STATE, "not running");
1496 }
1497
1498 // Broadcast ASCII frame to all connected clients
1499 size_t frame_len = strlen(frame);
1501
1502 mutex_lock(&host->clients_mutex);
1503 for (int i = 0; i < host->max_clients; i++) {
1504 if (host->clients[i].active && host->clients[i].transport) {
1505 asciichat_error_t send_result = acip_send_ascii_frame(host->clients[i].transport, &host->frame_workspace, frame,
1506 frame_len, 80, 24, "session-host");
1507 if (send_result != ASCIICHAT_OK) {
1508 log_warn("Failed to send ASCII frame to client %u", host->clients[i].client_id);
1509 result = send_result;
1510 }
1511 }
1512 }
1513 mutex_unlock(&host->clients_mutex);
1514
1515 return result;
1516}
asciichat_error_t acip_send_ascii_frame(acip_transport_t *transport, acip_frame_workspace_t *workspace, const char *text, size_t length, uint32_t width, uint32_t height, const char *client_id)

References acip_send_ascii_frame(), ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, log_warn, and SET_ERRNO.

◆ session_host_client_has_transport()

bool session_host_client_has_transport ( session_host_t * host,
uint32_t client_id )

#include <src/common/session/host.h>

Check if a specific client has an active ACIP transport.

Includes TCP, WebRTC, and WebSocket transports.

Parameters
hostHost handle (must not be NULL)
client_idClient ID to check
Returns
true if a transport is set, false otherwise

Definition at line 1776 of file host.c.

1776 {
1777 if (!host || !host->initialized) {
1778 return false;
1779 }
1780
1781 mutex_lock(&host->clients_mutex);
1782
1783 // Find client with matching ID
1784 for (int i = 0; i < host->max_clients; i++) {
1785 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1786 bool has_transport = host->clients[i].transport != NULL;
1787 mutex_unlock(&host->clients_mutex);
1788 return has_transport;
1789 }
1790 }
1791
1792 mutex_unlock(&host->clients_mutex);
1793 return false;
1794}

◆ session_host_create()

session_host_t * session_host_create ( const session_host_config_t * config)

#include <src/common/session/host.h>

Create a new session host.

Parameters
configHost configuration (must not be NULL)
Returns
Pointer to host handle, or NULL on failure

Creates a session host with the specified configuration. The host is not started until session_host_start() is called.

Note
Call session_host_destroy() to free resources when done.
On failure, sets asciichat_errno with error details.

Definition at line 234 of file host.c.

234 {
235 if (!config) {
236 SET_ERRNO(ERROR_INVALID_PARAM, "NULL config");
237 return NULL;
238 }
239
240 // Allocate host
242
243 // Copy configuration
244 host->port = config->port > 0 ? config->port : OPT_PORT_INT_DEFAULT;
245 host->max_clients = config->max_clients > 0 ? config->max_clients : SESSION_HOST_DEFAULT_MAX_CLIENTS;
246 host->encryption_enabled = config->encryption_enabled;
247 host->configured_quic_listeners = config->configured_quic_listeners;
248
249 if (config->ipv4_address) {
250 SAFE_STRNCPY(host->ipv4_address, config->ipv4_address, sizeof(host->ipv4_address));
251 }
252
253 if (config->ipv6_address) {
254 SAFE_STRNCPY(host->ipv6_address, config->ipv6_address, sizeof(host->ipv6_address));
255 }
256
257 if (config->key_path) {
258 SAFE_STRNCPY(host->key_path, config->key_path, sizeof(host->key_path));
259 }
260
261 if (config->password) {
262 SAFE_STRNCPY(host->password, config->password, sizeof(host->password));
263 }
264
265 host->callbacks = config->callbacks;
266 host->user_data = config->user_data;
267
268 host->running = false;
269
270 // Allocate client array
271 host->clients = SAFE_CALLOC((size_t)host->max_clients, sizeof(session_host_client_t), session_host_client_t *);
272
273 host->client_count = 0;
274 host->next_client_id = 1;
275
276 // Initialize mutex
277 if (mutex_init(&host->clients_mutex, "host_clients") != 0) {
278 SET_ERRNO(ERROR_THREAD, "Failed to initialize clients mutex");
279 SAFE_FREE(host->clients);
280 acip_frame_workspace_destroy(&host->frame_workspace);
281 SAFE_FREE(host);
282 return NULL;
283 }
284
285 host->initialized = true;
286 return host;
287}
@ ERROR_THREAD
#define OPT_PORT_INT_DEFAULT
Default TCP port for client/server communication (integer).
Definition options.h:567
#define SESSION_HOST_DEFAULT_MAX_CLIENTS
Default maximum clients.
Definition host.c:68
void acip_frame_workspace_destroy(acip_frame_workspace_t *workspace)
Keeps the host-side rendering history used to compare successive frames and stabilize output for conn...
Definition host.c:88
void * user_data
User-provided context pointer passed to callbacks.
Definition host.h:188
const char * ipv6_address
IPv6 address to bind to (NULL for any).
Definition host.h:167
session_host_callbacks_t callbacks
Event callbacks.
Definition host.h:185
bool configured_quic_listeners
Definition host.h:176
int port
Port to listen on (default: 27224).
Definition host.h:161
const char * ipv4_address
IPv4 address to bind to (NULL for any).
Definition host.h:164
int max_clients
Maximum number of clients.
Definition host.h:170

References acip_frame_workspace_destroy(), session_host_config_t::callbacks, session_host_config_t::configured_quic_listeners, session_host_config_t::encryption_enabled, ERROR_INVALID_PARAM, ERROR_THREAD, session_host_config_t::ipv4_address, session_host_config_t::ipv6_address, session_host_config_t::key_path, session_host_config_t::max_clients, OPT_PORT_INT_DEFAULT, session_host_config_t::password, session_host_config_t::port, SAFE_CALLOC, SAFE_FREE, SAFE_STRNCPY, SESSION_HOST_DEFAULT_MAX_CLIENTS, SET_ERRNO, and session_host_config_t::user_data.

◆ session_host_destroy()

asciichat_error_t session_host_destroy ( session_host_t * host)

#include <src/common/session/host.h>

Destroy session host and free resources.

Parameters
hostHost to destroy (can be NULL)

Stops if running, disconnects all clients, and releases all resources. Safe to call with NULL.

Definition at line 289 of file host.c.

289 {
290 if (!host)
291 return ASCIICHAT_OK;
293 if (result != ASCIICHAT_OK)
294 return result;
295
296 // Clean up audio resources
297 if (host->audio_ctx) {
298#ifndef __EMSCRIPTEN__
299 session_audio_destroy(host->audio_ctx);
300#endif
301 host->audio_ctx = NULL;
302 }
303
304 while (host->render_histories) {
305 host_render_history_t *history = host->render_histories;
306 host->render_histories = history->next;
307 ascii_stabilizer_reset(&history->stabilizer);
308 SAFE_FREE(history);
309 }
310
311 // Free client array and per-client resources
312 if (host->clients) {
313 for (int i = 0; i < host->max_clients; i++) {
314 host_client_release_transport(&host->clients[i]);
315 if (host->clients[i].incoming_video) {
316 image_destroy(host->clients[i].incoming_video);
317 host->clients[i].incoming_video = NULL;
318 }
319 if (host->clients[i].incoming_audio) {
320 ringbuffer_destroy(host->clients[i].incoming_audio);
321 host->clients[i].incoming_audio = NULL;
322 }
323 }
324 SAFE_FREE(host->clients);
325 }
326
327 // Destroy mutex
328 mutex_destroy(&host->clients_mutex);
329
330 // Clear sensitive data
331 memset(host->password, 0, sizeof(host->password));
332
333 host->initialized = false;
334 acip_frame_workspace_destroy(&host->frame_workspace);
335 SAFE_FREE(host);
336 return ASCIICHAT_OK;
337}
struct host_render_history host_render_history_t
Owns the server-side session state, including connected participants, host callbacks,...

References acip_frame_workspace_destroy(), ascii_stabilizer_reset(), ASCIICHAT_OK, image_destroy(), ringbuffer_destroy(), SAFE_FREE, session_audio_destroy(), and session_host_stop().

Referenced by discovery_session_apply_handoff(), discovery_session_destroy(), and BrowserHostState::reset().

◆ session_host_find_client()

asciichat_error_t session_host_find_client ( session_host_t * host,
uint32_t client_id,
session_host_client_info_t * info )

#include <src/common/session/host.h>

Find a client by ID.

Parameters
hostHost handle (must not be NULL)
client_idClient ID to find
infoOutput client info structure (must not be NULL)
Returns
ASCIICHAT_OK on success, ERROR_NOT_FOUND if not found

Retrieves information about a connected client.

Definition at line 1435 of file host.c.

1435 {
1436 if (!host || !host->initialized || !info) {
1437 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid parameter");
1438 }
1439
1440 mutex_lock(&host->clients_mutex);
1441
1442 for (int i = 0; i < host->max_clients; i++) {
1443 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1444 info->client_id = host->clients[i].client_id;
1445 SAFE_STRNCPY(info->ip_address, host->clients[i].ip_address, sizeof(info->ip_address));
1446 info->port = host->clients[i].port;
1447 info->video_active = host->clients[i].video_active;
1448 info->audio_active = host->clients[i].audio_active;
1449 info->connected_at = host->clients[i].connected_at;
1450
1451 mutex_unlock(&host->clients_mutex);
1452 return ASCIICHAT_OK;
1453 }
1454 }
1455
1456 mutex_unlock(&host->clients_mutex);
1457 return SET_ERRNO(ERROR_NOT_FOUND, "Client not found: %u", client_id);
1458}
uint64_t connected_at
Connection timestamp (Unix time).
Definition host.h:100
int port
Client port.
Definition host.h:91
bool audio_active
Client is currently streaming audio.
Definition host.h:97
char ip_address[IP_ADDRESS_BUFFER_SIZE]
Client IP address.
Definition host.h:88
uint32_t client_id
Unique client identifier.
Definition host.h:85
bool video_active
Client is currently streaming video.
Definition host.h:94

References ASCIICHAT_OK, session_host_client_info_t::audio_active, session_host_client_info_t::client_id, session_host_client_info_t::connected_at, ERROR_INVALID_PARAM, ERROR_NOT_FOUND, session_host_client_info_t::ip_address, session_host_client_info_t::port, SAFE_STRNCPY, SET_ERRNO, and session_host_client_info_t::video_active.

◆ session_host_get_client_count()

int session_host_get_client_count ( session_host_t * host)

#include <src/common/session/host.h>

Get number of connected clients.

Parameters
hostHost handle (can be NULL)
Returns
Number of connected clients, 0 if NULL

Definition at line 1460 of file host.c.

1460 {
1461 if (!host || !host->initialized) {
1462 return 0;
1463 }
1464 return host->client_count;
1465}

◆ session_host_get_client_ids()

int session_host_get_client_ids ( session_host_t * host,
uint32_t * ids,
int max_ids )

#include <src/common/session/host.h>

Get list of all connected client IDs.

Parameters
hostHost handle (must not be NULL)
idsOutput array for client IDs (must not be NULL)
max_idsMaximum number of IDs to return (size of ids array)
Returns
Number of IDs written to array

Definition at line 1467 of file host.c.

1467 {
1468 if (!host || !host->initialized || !ids || max_ids <= 0) {
1469 return 0;
1470 }
1471
1472 mutex_lock(&host->clients_mutex);
1473
1474 int count = 0;
1475 for (int i = 0; i < host->max_clients && count < max_ids; i++) {
1476 if (host->clients[i].active) {
1477 ids[count++] = host->clients[i].client_id;
1478 }
1479 }
1480
1481 mutex_unlock(&host->clients_mutex);
1482 return count;
1483}

◆ session_host_get_client_transport()

acip_transport_t * session_host_get_client_transport ( session_host_t * host,
uint32_t client_id )

#include <src/common/session/host.h>

Get the current transport for a specific client in the host.

Returns the currently active transport, regardless of its backend.

Parameters
hostHost handle (must not be NULL)
client_idClient ID whose transport should be retrieved
Returns
Borrowed transport pointer, NULL if no transport is registered or client not found

Definition at line 1756 of file host.c.

1756 {
1757 if (!host || !host->initialized) {
1758 return NULL;
1759 }
1760
1761 mutex_lock(&host->clients_mutex);
1762
1763 // Find client with matching ID
1764 for (int i = 0; i < host->max_clients; i++) {
1765 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1766 acip_transport_t *transport = host->clients[i].transport;
1767 mutex_unlock(&host->clients_mutex);
1768 return transport;
1769 }
1770 }
1771
1772 mutex_unlock(&host->clients_mutex);
1773 return NULL;
1774}
struct acip_transport acip_transport_t
Forward declaration of ACIP transport.
Definition host.h:506

Referenced by session_host_receive_packet().

◆ session_host_inject_audio()

asciichat_error_t session_host_inject_audio ( session_host_t * host,
uint32_t participant_id,
const float * samples,
size_t count )

#include <src/common/session/host.h>

Inject audio samples from memory participant.

Parameters
hostHost handle (must not be NULL)
participant_idMemory participant ID
samplesAudio sample buffer (float format, must not be NULL)
countNumber of samples
Returns
ASCIICHAT_OK on success, error code on failure

Injects audio samples directly into the mixer from a memory participant. This bypasses network I/O and is used when the host participates in the session with their own audio.

Note
Sample data is copied, so caller retains ownership.

Definition at line 1342 of file host.c.

1343 {
1344 if (!host || !host->initialized || !samples || count == 0 || count % AUDIO_CHANNELS) {
1345 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid parameters");
1346 }
1347
1348 mutex_lock(&host->clients_mutex);
1349
1350 // Find memory participant
1351 for (int i = 0; i < host->max_clients; i++) {
1352 if (host->clients[i].active && host->clients[i].client_id == participant_id &&
1353 host->clients[i].participant_type == PARTICIPANT_TYPE_MEMORY) {
1354
1355 if (!host->clients[i].incoming_audio) {
1356 mutex_unlock(&host->clients_mutex);
1357 return SET_ERRNO(ERROR_INVALID_STATE, "Memory participant has no audio buffer");
1358 }
1359
1360 // Write samples to ringbuffer (one at a time)
1361 size_t written = 0;
1362 for (size_t j = 0; j < count; j++) {
1363 if (ringbuffer_write(host->clients[i].incoming_audio, &samples[j])) {
1364 written++;
1365 } else {
1366 break; // Buffer full
1367 }
1368 }
1369
1370 if (written < count) {
1371 log_warn_every(NS_PER_MS_INT, "Audio ringbuffer full, dropped %zu samples", count - written);
1372 }
1373
1374 host->clients[i].audio_active = true;
1375
1376 mutex_unlock(&host->clients_mutex);
1377 return ASCIICHAT_OK;
1378 }
1379 }
1380
1381 mutex_unlock(&host->clients_mutex);
1382 return SET_ERRNO(ERROR_NOT_FOUND, "Memory participant not found");
1383}
bool ringbuffer_write(ringbuffer_t *rb, const void *data)
Try to write an element to the ring buffer (non-blocking).
Definition ringbuffer.c:71

References ASCIICHAT_OK, AUDIO_CHANNELS, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_NOT_FOUND, log_warn_every, NS_PER_MS_INT, participant_id, PARTICIPANT_TYPE_MEMORY, ringbuffer_write(), and SET_ERRNO.

◆ session_host_inject_frame()

asciichat_error_t session_host_inject_frame ( session_host_t * host,
uint32_t participant_id,
const image_t * frame )

#include <src/common/session/host.h>

Inject a video frame from memory participant.

Parameters
hostHost handle (must not be NULL)
participant_idMemory participant ID
frameVideo frame image (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Injects a video frame directly into the mixer from a memory participant. This bypasses network I/O and is used when the host participates in the session with their own webcam.

Note
Frame data is copied, so caller retains ownership.

Definition at line 1290 of file host.c.

1290 {
1291 if (!host || !host->initialized || !frame) {
1292 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid parameters");
1293 }
1294
1295 if (!frame->pixels || frame->w <= 0 || frame->h <= 0 ||
1296 image_validate_dimensions((size_t)frame->w, (size_t)frame->h) != ASCIICHAT_OK)
1297 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid injected image");
1298 size_t pixel_bytes;
1299 if (image_calc_rgb_size((size_t)frame->w, (size_t)frame->h, &pixel_bytes) != ASCIICHAT_OK)
1300 return SET_ERRNO(ERROR_INVALID_PARAM, "Injected image size is outside supported limits");
1301
1302 mutex_lock(&host->clients_mutex);
1303
1304 // Find memory participant
1305 for (int i = 0; i < host->max_clients; i++) {
1306 if (host->clients[i].active && host->clients[i].client_id == participant_id &&
1307 host->clients[i].participant_type == PARTICIPANT_TYPE_MEMORY) {
1308
1309 if (!host->clients[i].incoming_video) {
1310 mutex_unlock(&host->clients_mutex);
1311 return SET_ERRNO(ERROR_INVALID_STATE, "Memory participant has no video buffer");
1312 }
1313
1314 // Copy frame data into buffer
1315 image_t *dest = host->clients[i].incoming_video;
1316 if (dest->w != frame->w || dest->h != frame->h) {
1317 // Reallocate if size changed
1318 image_t *replacement = image_new(frame->w, frame->h);
1319 if (!replacement) {
1320 mutex_unlock(&host->clients_mutex);
1321 return SET_ERRNO(ERROR_MEMORY, "Failed to reallocate video buffer");
1322 }
1323 image_destroy(dest);
1324 dest = replacement;
1325 host->clients[i].incoming_video = dest;
1326 }
1327
1328 // Copy pixel data
1329 memcpy(dest->pixels, frame->pixels, pixel_bytes);
1330
1331 host->clients[i].video_active = true;
1332
1333 mutex_unlock(&host->clients_mutex);
1334 return ASCIICHAT_OK;
1335 }
1336 }
1337
1338 mutex_unlock(&host->clients_mutex);
1339 return SET_ERRNO(ERROR_NOT_FOUND, "Memory participant not found");
1340}
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 image_validate_dimensions(size_t width, size_t height)
Validate image dimensions (non-zero, within limits).
Definition util/image.c:100
asciichat_error_t image_calc_rgb_size(size_t width, size_t height, size_t *out_size)
Calculate total RGB buffer size from dimensions.
Definition util/image.c:54

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_MEMORY, ERROR_NOT_FOUND, image_t::h, image_calc_rgb_size(), image_destroy(), image_new(), image_validate_dimensions(), participant_id, PARTICIPANT_TYPE_MEMORY, image_t::pixels, SET_ERRNO, and image_t::w.

◆ session_host_is_running()

bool session_host_is_running ( session_host_t * host)

#include <src/common/session/host.h>

Check if host is running.

Parameters
hostHost handle (can be NULL)
Returns
true if running, false otherwise

Definition at line 1118 of file host.c.

1118 {
1119 if (!host || !host->initialized) {
1120 return false;
1121 }
1122 return host->running;
1123}

◆ session_host_remove_client()

asciichat_error_t session_host_remove_client ( session_host_t * host,
uint32_t client_id )

#include <src/common/session/host.h>

Remove a client by ID.

Parameters
hostHost handle (must not be NULL)
client_idClient ID to remove
Returns
ASCIICHAT_OK on success, error code on failure

Disconnects and removes a client from the session.

Note
on_client_leave callback is invoked before removal.

Definition at line 1385 of file host.c.

1385 {
1386 if (!host || !host->initialized) {
1387 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid host");
1388 }
1389
1390 mutex_lock(&host->clients_mutex);
1391
1392 for (int i = 0; i < host->max_clients; i++) {
1393 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1394 // Invoke callback before removing
1395 mutex_unlock(&host->clients_mutex);
1396 if (host->callbacks.on_client_leave) {
1397 host->callbacks.on_client_leave(host, client_id, host->user_data);
1398 }
1399 mutex_lock(&host->clients_mutex);
1400
1401#ifndef __EMSCRIPTEN__
1402 if (host->clients[i].connection) {
1403 network_connection_request_stop(host->clients[i].connection);
1404 mutex_unlock(&host->clients_mutex);
1405 return ASCIICHAT_OK;
1406 }
1407#endif
1408 host_client_release_transport(&host->clients[i]);
1409
1410 // Clean up media buffers
1411 if (host->clients[i].incoming_video) {
1412 image_destroy(host->clients[i].incoming_video);
1413 host->clients[i].incoming_video = NULL;
1414 }
1415 if (host->clients[i].incoming_audio) {
1416 ringbuffer_destroy(host->clients[i].incoming_audio);
1417 host->clients[i].incoming_audio = NULL;
1418 }
1419
1420 host_forget_render_history(host, client_id);
1421 host->clients[i].active = false;
1422 host->client_count--;
1423 if (host->render_target.client_id == client_id)
1424 host->render_target = (session_host_render_target_t){0};
1425
1426 mutex_unlock(&host->clients_mutex);
1427 return ASCIICHAT_OK;
1428 }
1429 }
1430
1431 mutex_unlock(&host->clients_mutex);
1432 return SET_ERRNO(ERROR_NOT_FOUND, "Client not found: %u", client_id);
1433}
void network_connection_request_stop(network_connection_t *c)
session host render target is intentionally a small, concrete record. Fields such as client_id,...
Definition host.h:450

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_NOT_FOUND, image_destroy(), network_connection_request_stop(), ringbuffer_destroy(), and SET_ERRNO.

◆ session_host_send_frame()

asciichat_error_t session_host_send_frame ( session_host_t * host,
uint32_t client_id,
const char * frame )

#include <src/common/session/host.h>

Send ASCII frame to a specific client.

Parameters
hostHost handle (must not be NULL)
client_idTarget client ID
frameASCII frame data (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Definition at line 1518 of file host.c.

1518 {
1519 if (!host || !host->initialized || !frame) {
1520 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid parameter");
1521 }
1522
1523 if (!host->running) {
1524 return SET_ERRNO(ERROR_INVALID_STATE, "not running");
1525 }
1526
1527 // Send ASCII frame to specific client
1528 size_t frame_len = strlen(frame);
1529
1530 mutex_lock(&host->clients_mutex);
1531 for (int i = 0; i < host->max_clients; i++) {
1532 if (host->clients[i].client_id == client_id && host->clients[i].active) {
1533 asciichat_error_t result = host->clients[i].transport
1534 ? acip_send_ascii_frame(host->clients[i].transport, &host->frame_workspace, frame,
1535 frame_len, 80, 24, "session-host")
1536 : SET_ERRNO(ERROR_INVALID_STATE, "Client %u has no transport", client_id);
1537 mutex_unlock(&host->clients_mutex);
1538 return result;
1539 }
1540 }
1541 mutex_unlock(&host->clients_mutex);
1542
1543 return SET_ERRNO(ERROR_NOT_FOUND, "client %u not found", client_id);
1544}

References acip_send_ascii_frame(), ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_NOT_FOUND, and SET_ERRNO.

◆ session_host_set_client_transport()

asciichat_error_t session_host_set_client_transport ( session_host_t * host,
uint32_t client_id,
acip_transport_t * transport,
acip_transport_ownership_t ownership )

#include <src/common/session/host.h>

Select a transport for a specific client in the host.

Selects the transport used for this client by all send/receive operations. Detaching clears the selection; no backend-specific fallback is retained.

Parameters
hostHost handle (must not be NULL)
client_idClient ID whose transport should be replaced
transportSelected transport, or NULL to detach without a fallback
ownershipResponsibility for destroying the selected transport
Returns
ASCIICHAT_OK on success, ERROR_NOT_FOUND if client not found, other error on failure
Note
Replacing an owned transport destroys it. Ownership transfers only on success.

Definition at line 1703 of file host.c.

1704 {
1705 if (!host || !host->initialized) {
1706 return SET_ERRNO(ERROR_INVALID_PARAM, "Host is NULL or not initialized");
1707 }
1708
1709 if ((ownership != ACIP_TRANSPORT_BORROWED && ownership != ACIP_TRANSPORT_OWNED) ||
1710 (!transport && ownership == ACIP_TRANSPORT_OWNED))
1711 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid transport ownership");
1712
1713 mutex_lock(&host->clients_mutex);
1714
1715 // Find client with matching ID
1716 for (int i = 0; i < host->max_clients; i++) {
1717 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1718 log_info("Setting transport=%p for client %u (was=%p)", transport, client_id, host->clients[i].transport);
1719
1720 if (host->clients[i].transport == transport && host->clients[i].transport_ownership != ownership) {
1721 mutex_unlock(&host->clients_mutex);
1722 return SET_ERRNO(ERROR_INVALID_STATE, "Cannot change ownership of a registered transport");
1723 }
1724 server_state_packet_t state = {0};
1725 if (transport) {
1726 uint32_t active_count = 0;
1727 for (int j = 0; j < host->max_clients; j++) {
1728 if (host->clients[j].active) {
1729 active_count++;
1730 }
1731 }
1732 state.connected_client_count = HOST_TO_NET_U32(active_count);
1733 state.active_client_count = HOST_TO_NET_U32(active_count);
1734 }
1735 if (transport) {
1736 asciichat_error_t result = acip_send_server_state(transport, &state);
1737 if (result != ASCIICHAT_OK) {
1738 mutex_unlock(&host->clients_mutex);
1739 return result;
1740 }
1741 }
1742 if (host->clients[i].transport != transport)
1743 host_client_release_transport(&host->clients[i]);
1744 host->clients[i].transport = transport;
1745 host->clients[i].transport_ownership = transport ? ownership : ACIP_TRANSPORT_BORROWED;
1746 mutex_unlock(&host->clients_mutex);
1747 return ASCIICHAT_OK;
1748 }
1749 }
1750
1751 mutex_unlock(&host->clients_mutex);
1752 log_warn("Client %u not found", client_id);
1753 return SET_ERRNO(ERROR_NOT_FOUND, "Client not found");
1754}
#define HOST_TO_NET_U32(val)
Definition endian.h:66
asciichat_error_t acip_send_server_state(acip_transport_t *transport, const server_state_packet_t *state)
Send server state update to client (server โ†’ client).
Server state packet structure.
Definition packet.h:746

References acip_send_server_state(), ACIP_TRANSPORT_BORROWED, ACIP_TRANSPORT_OWNED, ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_NOT_FOUND, HOST_TO_NET_U32, log_info, log_warn, and SET_ERRNO.

◆ session_host_set_display()

asciichat_error_t session_host_set_display ( session_host_t * host,
struct session_display_ctx * display )

#include <src/common/session/host.h>

Set a native terminal display context for local frame rendering.

Parameters
hostHost handle (must not be NULL)
displayDisplay context for rendering frames locally (can be NULL to disable)
Returns
ASCIICHAT_OK on success, error code on failure

Configures the host to render frames locally to the provided display context. This allows the host to see its own rendered output during testing. If display is NULL, local rendering is disabled.

Note
Can be called before or after render thread is started.

Definition at line 1614 of file host.c.

1614 {
1615 if (!host || !host->initialized)
1616 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid host");
1617 mutex_lock(&host->clients_mutex);
1618 host->display_context = display;
1619 host->render_target = display ? (session_host_render_target_t){.get_config = native_display_config,
1620 .render_frame = native_display_frame,
1621 .context = host}
1623 mutex_unlock(&host->clients_mutex);
1624 return ASCIICHAT_OK;
1625}

References ASCIICHAT_OK, ERROR_INVALID_PARAM, and SET_ERRNO.

◆ session_host_start()

asciichat_error_t session_host_start ( session_host_t * host)

#include <src/common/session/host.h>

Start accepting client connections.

Parameters
hostHost handle (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Binds to the configured port and starts accepting connections. This function may start background threads for connection handling.

Definition at line 1037 of file host.c.

1037 {
1038 if (!host || !host->initialized)
1039 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid session host");
1040 if (host->running)
1041 return ASCIICHAT_OK;
1042 if (host->port > UINT16_MAX)
1043 return SET_ERRNO(ERROR_INVALID_PARAM, "Host port is outside the transport endpoint range");
1044#ifdef __EMSCRIPTEN__
1045 return SET_ERRNO(ERROR_NOT_SUPPORTED, "Browser hosts accept browser-managed connections");
1046#else
1048 if (host->configured_quic_listeners)
1049 result = session_quic_listeners_open(host->port, &host->quic_endpoint, &host->webtransport_endpoint);
1050 if (result != ASCIICHAT_OK)
1051 return result;
1052 network_server_config_t config = {
1053 .quic_endpoint = host->quic_endpoint,
1054 .webtransport_endpoint = host->webtransport_endpoint,
1055 .port = host->port,
1056 .ipv4_address = host->ipv4_address[0] ? host->ipv4_address : "127.0.0.1",
1057 .bind_ipv4 = true,
1058 .max_connections = host->max_clients,
1059 .partial_timeout_ms = 30000,
1060 .callbacks = {host_connection_open, host_connection_packet, host_connection_closing, host_connection_closed},
1061 .user_data = host};
1062 result = network_server_create(&config, &host->network_server);
1063 if (result == ASCIICHAT_OK)
1064 result = network_server_start(host->network_server);
1065 if (result == ASCIICHAT_OK)
1066 result = platform_thread_create(&host->network_thread, "host_network", host_network_run, host);
1067 if (result != ASCIICHAT_OK) {
1068 network_server_destroy(&host->network_server);
1069 return result;
1070 }
1071 host->network_thread_started = true;
1072 host->running = true;
1073 return ASCIICHAT_OK;
1074#endif
1075}
@ ERROR_NOT_SUPPORTED
Definition error_codes.h:69
asciichat_error_t network_server_create(const network_server_config_t *config, network_server_t **out)
asciichat_error_t network_server_start(network_server_t *s)
void network_server_destroy(network_server_t **owner)
asciichat_error_t session_quic_listeners_open(int port, quic_endpoint_t **native, quic_endpoint_t **webtransport)

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_NOT_SUPPORTED, network_server_create(), network_server_destroy(), network_server_start(), session_quic_listeners_open(), and SET_ERRNO.

◆ session_host_start_render()

asciichat_error_t session_host_start_render ( session_host_t * host)

#include <src/common/session/host.h>

Start media rendering thread (video mixing and audio distribution).

Parameters
hostHost handle (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Starts the render thread which collects video frames from all participants, generates mixed ASCII frames, and broadcasts them to all clients. Also handles audio mixing and distribution.

Note
The host must be running before starting the render thread.
Call session_host_stop_render() to stop the thread.

Definition at line 1632 of file host.c.

1632 {
1633 if (!host || !host->initialized) {
1634 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid host");
1635 }
1636
1637 // Discovery mode starts the shared host renderer without passing through
1638 // server/main.c, which normally initializes the ASCII lookup palettes.
1640
1641 if (!host->running) {
1642 return SET_ERRNO(ERROR_INVALID_STATE, "not running");
1643 }
1644
1645 if (host->render_thread_running) {
1646 return ASCIICHAT_OK; // Already running
1647 }
1648
1649 // Create audio context for mixing (host mode = true)
1650 if (!host->audio_ctx) {
1651 host->audio_ctx = session_audio_create(true);
1652 if (!host->audio_ctx) {
1653 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to create audio context");
1654 }
1655 }
1656
1657 // Spawn render thread
1658 host->render_thread_running = true;
1659 if (platform_thread_create(&host->render_thread, "render", host_render_thread, host) != 0) {
1660 log_error("Failed to spawn render thread");
1661 host->render_thread_running = false;
1662 return SET_ERRNO(ERROR_THREAD, "Failed to spawn render thread");
1663 }
1664
1665 log_info("Host render thread started");
1666 return ASCIICHAT_OK;
1667}
void ascii_simd_init(void)
Initialize SIMD subsystem.

References ascii_simd_init(), ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_THREAD, log_error, log_info, session_audio_create(), and SET_ERRNO.

◆ session_host_stop()

asciichat_error_t session_host_stop ( session_host_t * host)

#include <src/common/session/host.h>

Stop accepting connections and disconnect all clients.

Parameters
hostHost handle (must not be NULL)

Gracefully stops the server, disconnects all clients, and cleans up.

Definition at line 1076 of file host.c.

1076 {
1077 if (!host || !host->initialized)
1078 return ASCIICHAT_OK;
1079 if (host->render_thread_running) {
1080 host->render_thread_running = false;
1081 asciichat_error_t result = thread_join(&host->render_thread, NULL);
1082 if (result != ASCIICHAT_OK) {
1083 host->render_thread_running = true;
1084 return result;
1085 }
1086 }
1087#ifndef __EMSCRIPTEN__
1088 network_server_request_stop(host->network_server);
1089 if (host->network_thread_started) {
1090 asciichat_error_t result = thread_join(&host->network_thread, NULL);
1091 if (result != ASCIICHAT_OK)
1092 return result;
1093 host->network_thread_started = false;
1094 }
1095 asciichat_error_t result = network_server_stop(host->network_server);
1096 if (result != ASCIICHAT_OK)
1097 return result;
1098 network_server_destroy(&host->network_server);
1099 result = session_quic_listeners_close(&host->quic_endpoint, &host->webtransport_endpoint);
1100 if (result != ASCIICHAT_OK)
1101 return result;
1102#endif
1103 host->running = false;
1104 return ASCIICHAT_OK;
1105}
void network_server_request_stop(network_server_t *s)
asciichat_error_t network_server_stop(network_server_t *s)
asciichat_error_t session_quic_listeners_close(quic_endpoint_t **native, quic_endpoint_t **webtransport)

References ASCIICHAT_OK, network_server_destroy(), network_server_request_stop(), network_server_stop(), and session_quic_listeners_close().

Referenced by session_host_destroy().

◆ session_host_stop_render()

void session_host_stop_render ( session_host_t * host)

#include <src/common/session/host.h>

Stop media rendering thread.

Parameters
hostHost handle (must not be NULL)

Gracefully stops the render thread and cleans up audio resources.

Definition at line 1669 of file host.c.

1669 {
1670 if (!host || !host->initialized) {
1671 return;
1672 }
1673
1674 if (!host->render_thread_running) {
1675 return;
1676 }
1677
1678 // Signal thread to stop
1679 host->render_thread_running = false;
1680
1681 // Wait for thread to complete
1682 if (thread_join(&host->render_thread, NULL) != ASCIICHAT_OK) {
1683 host->render_thread_running = true;
1684 SET_ERRNO(ERROR_THREAD, "Render worker still owns host audio resources");
1685 return;
1686 }
1687
1688 // Clean up audio resources
1689 if (host->audio_ctx) {
1690#ifndef __EMSCRIPTEN__
1691 session_audio_destroy(host->audio_ctx);
1692#endif
1693 host->audio_ctx = NULL;
1694 }
1695
1696 log_info("Host render thread stopped");
1697}

References ASCIICHAT_OK, ERROR_THREAD, log_info, session_audio_destroy(), and SET_ERRNO.

◆ session_mirror_capture_create()

session_capture_ctx_t * session_mirror_capture_create ( const session_capture_config_t * config)

#include <src/common/session/capture.h>

Create a new session capture context.

Parameters
configCapture configuration (must not be NULL)
Returns
Pointer to capture context, or NULL on failure

Creates and initializes a session capture context with the specified configuration. The media source is opened and ready for reading.

Note
Call session_capture_destroy() to free resources when done.
On failure, sets asciichat_errno with error details.

Create mirror mode capture context with local media source

Creates a capture context for local media (webcam, file, URL, test pattern). Used by mirror mode and snapshot operations that need to capture video locally.

Parameters
configCapture configuration (type, path, FPS, etc.)
Returns
Capture context with media source, or NULL on error
Note
Call session_capture_destroy() to free resources when done.
On failure, sets asciichat_errno with error details.

Definition at line 247 of file common/session/capture.c.

247 {
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}

References ERROR_INVALID_PARAM, session_capture_create(), and SET_ERRNO.

◆ session_network_capture_create()

session_capture_ctx_t * session_network_capture_create ( uint32_t target_fps)

#include <src/common/session/capture.h>

Create network mode capture context without media source.

Creates a minimal capture context for network modes (client, discovery) that receive video frames from the network instead of capturing locally. Only initializes keyboard and audio support; no media source is created.

Parameters
target_fpsTarget FPS (0 = default 60 FPS)
Returns
Capture context without media source, or NULL on error
Note
Call session_capture_destroy() to free resources when done.
On failure, sets asciichat_errno with error details.

Definition at line 257 of file common/session/capture.c.

257 {
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}

References session_capture_ctx::audio_enabled, CAPTURE_TRACKER_NAME_BUFFER_SIZE, DEFAULT_MAX_FPS, fps_init(), session_capture_ctx::fps_tracker, session_capture_ctx::initialized, log_debug, session_capture_ctx::resize_for_network, SAFE_CALLOC, SAFE_FREE, SAFE_MALLOC, safe_snprintf(), session_capture_ctx::source, session_capture_ctx::start_time_ns, and session_capture_ctx::target_fps.

◆ session_participant_connect()

asciichat_error_t session_participant_connect ( session_participant_t * p)

#include <src/common/session/participant.h>

Connect to session server.

Parameters
pParticipant handle (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Initiates connection to the configured server. Connection establishment is synchronous but callbacks are invoked asynchronously.

Note
on_connected callback is invoked on successful connection.
on_error callback is invoked on failure.

Definition at line 252 of file participant.c.

252 {
253 log_debug("p=%p", p);
254
255 if (!p || !p->initialized) {
256 log_debug("invalid param or not initialized");
257 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid participant");
258 }
259
260 log_debug("p->initialized=%d, p->connected=%d", p->initialized, p->connected);
261
262 if (p->connected) {
263 log_debug("already connected, returning OK");
264 return ASCIICHAT_OK; // Already connected
265 }
266 if (p->port > UINT16_MAX)
267 return SET_ERRNO(ERROR_INVALID_PARAM, "Participant port is outside the transport endpoint range");
268
270 if (result != ASCIICHAT_OK)
271 return result;
272 participant_connection_init(p);
273 char password[PASSWORD_BUFFER_SIZE] = {0};
274 SAFE_STRNCPY(password, p->password, sizeof(password));
275 network_connect_config_t quic_config = {.timeout_ms = 10000,
276 .tls_ca_file = p->tls_ca_file[0] ? p->tls_ca_file : NULL,
277 .quic_audio_datagrams = p->quic_audio_datagrams,
278 .quic_visual_datagrams = p->quic_visual_datagrams};
279 const network_connect_config_t *connect_config = strncmp(p->address, "quic://", 7) == 0 ? &quic_config : NULL;
280 result = session_connection_establish("participant", p->address, p->port, connect_config, &p->network, password,
281 sizeof(password), participant_prepare, NULL, p);
282 sodium_memzero(password, sizeof(password));
285 if (result == ASCIICHAT_OK)
287 if (result != ASCIICHAT_OK) {
291 return result;
292 }
293 log_debug("session_participant_connect: setting p->connected=true");
294
295 mutex_lock(&p->settings_mutex);
296 p->settings_request = 0;
298 p->settings.version = 0;
299 mutex_unlock(&p->settings_mutex);
300 p->connected = true;
301 p->client_id = 0; // Will be assigned by server
302
303 log_info("Connected to server at %s:%d", p->address, p->port);
304
305 // Invoke callback
306 if (p->callbacks.on_connected) {
308 }
309
310 return ASCIICHAT_OK;
311}
void errno_scope_end(asciichat_errno_scope_t scope, asciichat_errno_outcome_t outcome)
asciichat_errno_scope_t errno_scope_begin(void)
@ ASCIICHAT_ERRNO_HANDLED
acip_transport_t * network_connection_transport(network_connection_t *c)
#define PASSWORD_BUFFER_SIZE
Password input buffer size (256 bytes).
Definition sizes.h:158
asciichat_error_t session_connection_stop(session_connection_t *connection)
asciichat_error_t session_connection_start(session_connection_t *connection)
asciichat_error_t session_connection_establish(const char *name, const char *address, uint16_t port, const network_connect_config_t *config, network_connection_t **owner, char *password, size_t capacity, network_connection_prepare_fn prepare, void(*detach)(void *), void *context)
For network connect config, this is the live record that ties timeout_ms, callback_data,...
void(* on_connected)(session_participant_t *p, uint32_t client_id, void *user_data)
Called when successfully connected to session.
Definition participant.h:90
session_settings_t settings
Current session settings.
bool connected
Currently connected.
Definition participant.c:98
struct acip_transport * transport
Selected transport used by all media workers.
Definition participant.c:91
uint32_t settings_applied_request
network_connection_t * network
Definition participant.c:92
char password[BUFFER_SIZE_SMALL]
Password (if any).
Definition participant.c:70
char tls_ca_file[OPTIONS_BUFF_SIZE]
Definition participant.c:75
int port
Server port.
Definition participant.c:64
session_connection_t connection
Definition participant.c:59
char address[BUFFER_SIZE_SMALL]
Server address.
Definition participant.c:61
acip_transport_ownership_t transport_ownership
Responsibility for releasing the selected transport.
Definition participant.c:95
bool initialized
Context is initialized.
session_participant_callbacks_t callbacks
Event callbacks.
Definition participant.c:85
void * user_data
User data for callbacks.
Definition participant.c:88
uint32_t client_id
Assigned client ID.
uint32_t settings_request

References ACIP_TRANSPORT_BORROWED, ACIP_TRANSPORT_OWNED, session_participant::address, ASCIICHAT_ERRNO_HANDLED, ASCIICHAT_OK, session_participant::callbacks, session_participant::client_id, session_participant::connected, session_participant::connection, errno_scope_begin(), errno_scope_end(), ERROR_INVALID_PARAM, session_participant::initialized, log_debug, log_info, session_participant::network, network_connection_transport(), session_participant_callbacks_t::on_connected, session_participant::password, PASSWORD_BUFFER_SIZE, session_participant::port, session_participant::quic_audio_datagrams, session_participant::quic_visual_datagrams, SAFE_STRNCPY, session_connection_establish(), session_connection_start(), session_connection_stop(), SET_ERRNO, session_participant::settings, session_participant::settings_applied_request, session_participant::settings_mutex, session_participant::settings_request, session_participant::tls_ca_file, session_participant::transport, session_participant::transport_ownership, session_participant::user_data, and session_settings_t::version.

◆ session_participant_create()

session_participant_t * session_participant_create ( const session_participant_config_t * config)

#include <src/common/session/participant.h>

Server address, security, media, transport, and callback settings for a participant.

Create a new session participant

Parameters
configParticipant configuration (must not be NULL)
Returns
Pointer to participant handle, or NULL on failure

Creates a session participant with the specified configuration. The participant is not connected until session_participant_connect() is called.

Note
Call session_participant_destroy() to free resources when done.
On failure, sets asciichat_errno with error details.

Definition at line 156 of file participant.c.

156 {
157 log_info("config=%p", config);
158
159 if (!config) {
160 log_error("config is NULL");
161 SET_ERRNO(ERROR_INVALID_PARAM, "NULL config");
162 return NULL;
163 }
164
165 // Allocate participant
167 log_info("allocated p=%p", p);
168
169 // Copy configuration
170 if (config->address) {
171 SAFE_STRNCPY(p->address, config->address, sizeof(p->address));
172 } else {
173 SAFE_STRNCPY(p->address, "127.0.0.1", sizeof(p->address));
174 }
175
176 p->port = config->port > 0 ? config->port : OPT_PORT_INT_DEFAULT;
178
179 log_info("address=%s, port=%d", p->address, p->port);
180
181 if (config->password) {
182 SAFE_STRNCPY(p->password, config->password, sizeof(p->password));
183 }
184
185 if (config->server_key) {
186 SAFE_STRNCPY(p->server_key, config->server_key, sizeof(p->server_key));
187 }
188 if (config->tls_ca_file)
189 SAFE_STRNCPY(p->tls_ca_file, config->tls_ca_file, sizeof(p->tls_ca_file));
192
193 p->enable_audio = config->enable_audio;
194 p->enable_video = config->enable_video;
195 p->callbacks = config->callbacks;
196 p->user_data = config->user_data;
197
198 p->transport = NULL;
199 p->connected = false;
200 p->client_id = 0;
201
202 // Initialize settings
204 if (mutex_init(&p->settings_mutex, "participant_settings") != 0) {
205 SAFE_FREE(p);
206 return NULL;
207 }
208
209 participant_connection_init(p);
210 p->initialized = true;
211 log_info("returning p=%p (initialized=%d, connected=%d)", p, p->initialized, p->connected);
212 return p;
213}
void session_settings_init(session_settings_t *settings)
session_participant_callbacks_t callbacks
Event callbacks.
const char * address
Server address to connect to.
bool enable_video
Enable video capture and streaming.
const char * password
Password for server authentication (optional).
bool encryption_enabled
Enable encryption (default: true).
void * user_data
User-provided context pointer passed to callbacks.
const char * server_key
Expected server key for verification (optional).
bool enable_audio
Enable audio streaming.
int port
Server port (default: 27224).
bool encryption_enabled
Encryption enabled.
Definition participant.c:67
bool enable_audio
Audio enabled.
Definition participant.c:79
char server_key[BUFFER_SIZE_MEDIUM]
Server key for verification (if any).
Definition participant.c:73
bool enable_video
Video enabled.
Definition participant.c:82

References session_participant::address, session_participant_config_t::address, session_participant::callbacks, session_participant_config_t::callbacks, session_participant::client_id, session_participant::connected, session_participant::enable_audio, session_participant_config_t::enable_audio, session_participant::enable_video, session_participant_config_t::enable_video, session_participant::encryption_enabled, session_participant_config_t::encryption_enabled, ERROR_INVALID_PARAM, session_participant::initialized, log_error, log_info, OPT_PORT_INT_DEFAULT, session_participant::password, session_participant_config_t::password, session_participant::port, session_participant_config_t::port, session_participant::quic_audio_datagrams, session_participant_config_t::quic_audio_datagrams, session_participant::quic_visual_datagrams, session_participant_config_t::quic_visual_datagrams, SAFE_CALLOC, SAFE_FREE, SAFE_STRNCPY, session_participant::server_key, session_participant_config_t::server_key, session_settings_init(), SET_ERRNO, session_participant::settings, session_participant::settings_mutex, session_participant::tls_ca_file, session_participant_config_t::tls_ca_file, session_participant::transport, session_participant::user_data, and session_participant_config_t::user_data.

◆ session_participant_destroy()

void session_participant_destroy ( session_participant_t * p)

#include <src/common/session/participant.h>

Destroy session participant and free resources.

Parameters
pParticipant to destroy (can be NULL)

Disconnects if connected, stops all streams, and releases all resources. Safe to call with NULL.

Definition at line 215 of file participant.c.

215 {
216 if (!p) {
217 return;
218 }
219
221 // A failed join retains the participant and its resources for a safe retry.
222 if (atomic_load_int(&p->connection.state) != SESSION_CONNECTION_STOPPED || p->video_thread_created ||
224 return;
225
226 // Clear sensitive data
227 sodium_memzero(p->password, sizeof(p->password));
228 memset(p->server_key, 0, sizeof(p->server_key));
229
230 p->initialized = false;
231 mutex_destroy(&p->settings_mutex);
232 SAFE_FREE(p);
233}
bool audio_thread_created
Audio worker handle requires joining.
bool video_thread_created
Video worker handle requires joining.

References session_participant::audio_thread_created, session_participant::connection, session_participant::initialized, session_participant::password, SAFE_FREE, session_participant::server_key, SESSION_CONNECTION_STOPPED, session_participant_disconnect(), session_participant::settings_mutex, session_connection_t::state, and session_participant::video_thread_created.

Referenced by discovery_session_apply_handoff(), and discovery_session_destroy().

◆ session_participant_disconnect()

void session_participant_disconnect ( session_participant_t * p)

#include <src/common/session/participant.h>

Disconnect from session server.

Parameters
pParticipant handle (must not be NULL)

Gracefully disconnects from the server and stops all streams.

Note
on_disconnected callback is invoked after disconnection.

Definition at line 313 of file participant.c.

313 {
314 if (!p || !p->initialized) {
315 return;
316 }
317
318 bool was_connected = p->connected;
320 return;
321
322 // Invoke callback
323 if (was_connected && p->callbacks.on_disconnected) {
325 }
326}
void(* on_disconnected)(session_participant_t *p, void *user_data)
Called when disconnected from session.
Definition participant.h:97

References ASCIICHAT_OK, session_participant::callbacks, session_participant::connected, session_participant::connection, session_participant::initialized, session_participant_callbacks_t::on_disconnected, session_connection_stop(), and session_participant::user_data.

Referenced by session_participant_destroy().

◆ session_participant_get_client_id()

uint32_t session_participant_get_client_id ( session_participant_t * p)

#include <src/common/session/participant.h>

Get assigned client ID.

Parameters
pParticipant handle (can be NULL)
Returns
Client ID if connected, 0 otherwise

Definition at line 360 of file participant.c.

360 {
361 if (!p || !p->initialized || !p->connected) {
362 return 0;
363 }
364 return p->client_id;
365}

References session_participant::client_id, session_participant::connected, and session_participant::initialized.

◆ session_participant_get_settings()

asciichat_error_t session_participant_get_settings ( session_participant_t * p,
session_settings_t * settings )

#include <src/common/session/participant.h>

Get current session settings.

Parameters
pParticipant handle (must not be NULL)
settingsOutput settings structure (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Definition at line 383 of file participant.c.

383 {
384 if (!p || !p->initialized || !settings) {
385 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid parameter");
386 }
387
388 mutex_lock(&p->settings_mutex);
389 memcpy(settings, &p->settings, sizeof(session_settings_t));
390 mutex_unlock(&p->settings_mutex);
391 return ASCIICHAT_OK;
392}
When code needs to work with session settings, it keeps version, width, height, color_mode here inste...

References ASCIICHAT_OK, ERROR_INVALID_PARAM, session_participant::initialized, SET_ERRNO, session_participant::settings, and session_participant::settings_mutex.

◆ session_participant_get_transport()

acip_transport_t * session_participant_get_transport ( session_participant_t * p)

#include <src/common/session/participant.h>

Get the current transport for the participant.

Returns the selected transport, regardless of its backend. This accessor does not transfer ownership. External owners must detach their transport before freeing it; the participant destroys any owned transport.

Parameters
pParticipant handle (must not be NULL)
Returns
Selected transport, or NULL when none is attached

Definition at line 799 of file participant.c.

799 {
800 if (!p || !p->initialized) {
801 return NULL;
802 }
803
804 return p->transport;
805}

References session_participant::initialized, and session_participant::transport.

Referenced by discovery_session_check_host_alive().

◆ session_participant_has_transport()

bool session_participant_has_transport ( session_participant_t * p)

#include <src/common/session/participant.h>

Check if participant has a selected transport.

Includes TCP, WebRTC, and WebSocket transports.

Parameters
pParticipant handle (can be NULL)
Returns
true if a transport is selected, false otherwise

Definition at line 807 of file participant.c.

807 {
808 if (!p) {
809 return false;
810 }
811
812 return p->transport != NULL;
813}

References session_participant::transport.

◆ session_participant_is_audio_active()

bool session_participant_is_audio_active ( session_participant_t * p)

#include <src/common/session/participant.h>

Check if audio is streaming.

Parameters
pParticipant handle (can be NULL)
Returns
true if audio is active, false otherwise

Definition at line 375 of file participant.c.

375 {
376 return p && p->initialized && session_av_state_is_active(&p->audio);
377}
bool session_av_state_is_active(session_av_state_t *state)
Definition av_state.c:41
session_av_state_t audio
Audio streaming active.

References session_participant::audio, session_participant::initialized, and session_av_state_is_active().

◆ session_participant_is_connected()

bool session_participant_is_connected ( session_participant_t * p)

#include <src/common/session/participant.h>

Check if participant is connected.

Parameters
pParticipant handle (can be NULL)
Returns
true if connected, false otherwise

Definition at line 353 of file participant.c.

353 {
354 if (!p || !p->initialized) {
355 return false;
356 }
357 return p->connected;
358}

References session_participant::connected, and session_participant::initialized.

◆ session_participant_is_video_active()

bool session_participant_is_video_active ( session_participant_t * p)

#include <src/common/session/participant.h>

Check if video is streaming.

Parameters
pParticipant handle (can be NULL)
Returns
true if video is active, false otherwise

Definition at line 371 of file participant.c.

371 {
372 return p && p->initialized && session_av_state_is_active(&p->video);
373}
session_av_state_t video
Video streaming active.

References session_participant::initialized, session_av_state_is_active(), and session_participant::video.

◆ session_participant_request_settings()

asciichat_error_t session_participant_request_settings ( session_participant_t * p,
const session_settings_t * settings )

#include <src/common/session/participant.h>

Request this participant's remote rendering update.

Parameters
pParticipant handle (must not be NULL)
settingsNew settings to request (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Requests rendering for this participant. The host may accept or reject the request.

Note
Actual settings change is signaled via on_settings_changed callback.

Definition at line 394 of file participant.c.

394 {
395 if (!p || !p->initialized || !settings) {
396 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid parameter");
397 }
398
399 if (!p->connected) {
400 return SET_ERRNO(ERROR_INVALID_STATE, "not connected");
401 }
402
403 options_t *values = SAFE_MALLOC(sizeof(*values), options_t *);
404 session_settings_to_options(settings, values);
405 if (values->fps == 0)
406 values->fps = DEFAULT_MAX_FPS;
407 if (values->color_mode == TERM_COLOR_AUTO) {
408 terminal_capabilities_t caps = detect_terminal_capabilities(options_terminal_settings());
409 values->color_mode = caps.detection_reliable ? caps.color_level : TERM_COLOR_NONE;
410 }
411 acip_render_settings_t prepared = {0};
412 asciichat_error_t result = acip_render_settings_prepare(&prepared, values);
413 if (result == ASCIICHAT_OK) {
414 mutex_lock(&p->settings_mutex);
415 uint32_t request = ++p->settings_request;
416 mutex_unlock(&p->settings_mutex);
417 result = acip_settings_send(p->transport, values, request, UINT32_MAX);
418 }
419 SAFE_FREE(values);
420 return result;
421}
struct options_state options_t
Immutable, RCU-published snapshot of the application's runtime configuration.
asciichat_error_t acip_settings_send(acip_transport_t *transport, const options_t *values, uint32_t request, uint32_t expected_revision)
asciichat_error_t acip_render_settings_prepare(acip_render_settings_t *candidate, const options_t *values)
void session_settings_to_options(const session_settings_t *settings, options_t *values)
acip render settings keeps the values that move through the subsystem: caps, width,...
terminal_color_mode_t color_mode
Color mode (auto/none/16/256/truecolor).
Definition options.h:1092
int fps
Target framerate (0 = auto, 1-144).
Definition options.h:1099

References acip_render_settings_prepare(), acip_settings_send(), ASCIICHAT_OK, options_state::color_mode, session_participant::connected, DEFAULT_MAX_FPS, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, options_state::fps, session_participant::initialized, options_terminal_settings, SAFE_FREE, SAFE_MALLOC, session_settings_to_options(), SET_ERRNO, session_participant::settings_mutex, session_participant::settings_request, and session_participant::transport.

◆ session_participant_set_transport()

asciichat_error_t session_participant_set_transport ( session_participant_t * p,
acip_transport_t * transport,
acip_transport_ownership_t ownership )

#include <src/common/session/participant.h>

Select a transport and its ownership mode for the participant.

Selects the transport used by all participant media workers. Native connections use the library connection factory; negotiated transports use this same API.

Parameters
pParticipant handle (must not be NULL)
transportSelected transport, or NULL to detach without a fallback
ownershipResponsibility for destroying the selected transport
Returns
ASCIICHAT_OK on success, error code on failure
Note
This is used for WebRTC integration when DataChannel becomes ready
Ownership transfers only on success. Replacing an owned transport destroys it.
Media must be stopped before switching transports.

Definition at line 723 of file participant.c.

724 {
725 if (!p || !p->initialized) {
726 return SET_ERRNO(ERROR_INVALID_PARAM, "Participant is NULL or not initialized");
727 }
728
731 return SET_ERRNO(ERROR_INVALID_STATE, "Stop media before replacing its transport");
732
733 log_info("Setting transport=%p (was=%p)", transport, p->transport);
734
735 if (ownership != ACIP_TRANSPORT_BORROWED && ownership != ACIP_TRANSPORT_OWNED)
736 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid transport ownership");
737 if (transport && transport == p->transport) {
738 if (ownership != p->transport_ownership)
739 return SET_ERRNO(ERROR_INVALID_STATE, "Detach before transferring transport ownership");
740 return ASCIICHAT_OK;
741 }
743 if (result != ASCIICHAT_OK)
744 return result;
745 participant_connection_init(p);
746 if (transport && ownership == ACIP_TRANSPORT_OWNED) {
747 result = network_connection_adopt(transport, &p->network);
748 if (result != ASCIICHAT_OK)
749 return result;
750 }
751 p->transport = transport;
752 mutex_lock(&p->settings_mutex);
753 p->settings_request = 0;
755 p->settings.version = 0;
756 mutex_unlock(&p->settings_mutex);
757 p->transport_ownership = transport ? ownership : ACIP_TRANSPORT_BORROWED;
758 if (transport) {
760 if (result != ASCIICHAT_OK)
761 return result;
762 }
763 p->connected = p->transport && acip_transport_is_connected(p->transport);
764
765 return ASCIICHAT_OK;
766}
bool session_av_state_should_run(session_av_state_t *state)
Definition av_state.c:45
asciichat_error_t network_connection_adopt(acip_transport_t *transport, network_connection_t **out)

References ACIP_TRANSPORT_BORROWED, ACIP_TRANSPORT_OWNED, ASCIICHAT_OK, session_participant::audio, session_participant::audio_thread_created, session_participant::connected, session_participant::connection, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, session_participant::initialized, log_info, session_participant::network, network_connection_adopt(), session_av_state_should_run(), session_connection_start(), session_connection_stop(), SET_ERRNO, session_participant::settings, session_participant::settings_applied_request, session_participant::settings_mutex, session_participant::settings_request, session_participant::transport, session_participant::transport_ownership, session_settings_t::version, session_participant::video, and session_participant::video_thread_created.

Referenced by session_participant_set_connection().

◆ session_participant_start_audio()

asciichat_error_t session_participant_start_audio ( session_participant_t * p)

#include <src/common/session/participant.h>

Start audio capture and streaming.

Parameters
pParticipant handle (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Starts capturing audio from the microphone and streaming to the server. Also starts audio playback for receiving audio from other participants. Failure releases partially initialized capture/codec resources. Stop and disconnect join workers before releasing their resources.

Definition at line 625 of file participant.c.

625 {
626 if (!p || !p->initialized) {
627 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid participant");
628 }
629
630 if (!p->connected) {
631 return SET_ERRNO(ERROR_INVALID_STATE, "not connected");
632 }
633
634 if (!p->enable_audio) {
635 return SET_ERRNO(ERROR_INVALID_STATE, "audio not enabled");
636 }
637
639 return ASCIICHAT_OK;
640 uint32_t generation = session_av_state_begin(&p->audio);
641 if (!generation)
642 return SET_ERRNO(ERROR_INVALID_STATE, "audio is starting or stopping");
643
644 // Create audio capture context if not already created
645 if (!p->audio_capture) {
647 p->audio_capture = session_audio_create(false); // false = participant mode (no mixing)
648 if (!p->audio_capture) {
649 asciichat_error_t error = participant_startup_error(checkpoint, "Failed to create audio capture context");
651 return error;
652 }
653 }
654
656 p->audio_uses_video = source != NULL;
659
660 // Start audio capture and playback
662 if (err != ASCIICHAT_OK) {
664 return SET_ERRNO(err, "Failed to start audio duplex");
665 }
666
667 // Create Opus encoder for audio compression (48kHz, VOIP mode, 24 kbps)
668 if (!p->opus_encoder) {
672 p->opus_encoder =
674 profile.opus_bitrate, profile.encoder_channels);
675 if (!p->opus_encoder) {
676 asciichat_error_t error = participant_startup_error(checkpoint, "Failed to create Opus encoder");
678 return error;
679 }
680 }
681
682 // Spawn audio capture thread
683 if (platform_thread_create(&p->audio_capture_thread, "audio_capture", participant_audio_capture_thread, p) != 0) {
684 log_error("Failed to spawn audio capture thread");
686 return SET_ERRNO(ERROR_THREAD, "Failed to spawn audio capture thread");
687 }
688
689 p->audio_thread_created = true;
690 session_av_state_complete(&p->audio, generation);
691 log_info("Audio capture started");
692 return ASCIICHAT_OK;
693}
#define AUDIO_SAMPLE_RATE
Definition audio/limits.h:4
bool session_av_state_complete(session_av_state_t *state, uint32_t generation)
Definition av_state.c:34
uint32_t session_av_state_begin(session_av_state_t *state)
Definition av_state.c:22
void session_capture_set_audio_context(session_capture_ctx_t *ctx, void *audio_ctx)
Set the audio context for playback control.
opus_application_t
State for one Opus encoding or decoding direction.
Definition opus.h:84
opus_codec_t * opus_codec_create_encoder_channels(opus_application_t application, int sample_rate, int bitrate, int channels)
Definition opus.c:39
void session_participant_stop_audio(session_participant_t *p)
Stop audio capture and streaming.
void * session_capture_get_media_source(session_capture_ctx_t *ctx)
Get the underlying media source from capture context.
void * session_audio_get_context(session_audio_ctx_t *ctx)
void session_audio_set_capture_source(session_audio_ctx_t *ctx, void *source)
int encoder_channels
Transmitted channels, independent of playback.
opus_codec_t * opus_encoder
Opus encoder for audio compression.
platform_thread_t audio_capture_thread
Audio capture thread handle.
session_capture_ctx_t * video_capture
Video capture context (for webcam/file media).
session_audio_ctx_t * audio_capture
Audio capture context (for microphone).

References ASCIICHAT_OK, session_participant::audio, session_participant::audio_capture, session_participant::audio_capture_thread, AUDIO_SAMPLE_RATE, session_participant::audio_thread_created, session_participant::audio_uses_video, client_audio_pipeline_config_for_source(), session_participant::connected, session_participant::enable_audio, client_audio_pipeline_config_t::encoder_channels, errno_checkpoint(), ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_THREAD, session_participant::initialized, log_error, log_info, media_source_has_audio(), client_audio_pipeline_config_t::opus_application, client_audio_pipeline_config_t::opus_bitrate, opus_codec_create_encoder_channels(), session_participant::opus_encoder, session_audio_create(), session_audio_get_context(), session_audio_set_capture_source(), session_audio_start_duplex(), session_av_state_begin(), session_av_state_complete(), session_av_state_is_active(), session_capture_get_media_source(), session_capture_set_audio_context(), session_participant_stop_audio(), SET_ERRNO, source, and session_participant::video_capture.

◆ session_participant_start_video()

asciichat_error_t session_participant_start_video ( session_participant_t * p)

#include <src/common/session/participant.h>

Start video capture and streaming.

Parameters
pParticipant handle (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Starts the configured webcam/file source and its transmission worker. Repeated starts are idempotent. Failure releases resources and leaves video inactive. Control calls must be serialized by the owning thread.

Definition at line 541 of file participant.c.

541 {
542 if (!p || !p->initialized) {
543 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid participant");
544 }
545
546 if (!p->connected) {
547 return SET_ERRNO(ERROR_INVALID_STATE, "not connected");
548 }
549
550 if (!p->enable_video) {
551 return SET_ERRNO(ERROR_INVALID_STATE, "video not enabled");
552 }
553
555 return ASCIICHAT_OK;
556 uint32_t generation = session_av_state_begin(&p->video);
557 if (!generation)
558 return SET_ERRNO(ERROR_INVALID_STATE, "video is starting or stopping");
559
560 // Create video capture context if not already created
561 if (!p->video_capture) {
562 session_capture_config_t config = {
563 .type = MEDIA_SOURCE_WEBCAM,
564 .path = "0", // Default device
565 .target_fps = (uint32_t)GET_OPTION(fps),
566 .resize_for_network = true, // Optimize for bandwidth
567 };
568 const char *file = GET_OPTION(media_file);
569 const char *url = GET_OPTION(media_url);
571 safe_snprintf(webcam_index, sizeof(webcam_index), "%u", GET_OPTION(webcam_index));
572 config.path = webcam_index;
573 if (url && url[0]) {
574 config.type = MEDIA_SOURCE_FILE;
575 config.path = url;
576 } else if (file && file[0]) {
577 config.type = strcmp(file, "-") == 0 ? MEDIA_SOURCE_STDIN : MEDIA_SOURCE_FILE;
578 config.path = config.type == MEDIA_SOURCE_STDIN ? NULL : file;
579 }
580 config.loop = GET_OPTION(media_loop);
583 if (!p->video_capture) {
584 asciichat_error_t error = participant_startup_error(checkpoint, "Failed to create video capture context");
586 return error;
587 }
588 }
589
590 // Spawn video capture thread (media source is ready in ctx)
591 if (platform_thread_create(&p->video_capture_thread, "video_capture", participant_video_capture_thread, p) != 0) {
592 log_error("Failed to spawn video capture thread");
594 return SET_ERRNO(ERROR_THREAD, "Failed to spawn video capture thread");
595 }
596
597 p->video_thread_created = true;
598 session_av_state_complete(&p->video, generation);
599 log_info("Video capture started");
600 return ASCIICHAT_OK;
601}
void session_participant_stop_video(session_participant_t *p)
Stop and join video capture, then release its resources Also stops audio when it may borrow the video...
#define SESSION_PARTICIPANT_WEBCAM_INDEX_BUFFER_SIZE
Definition participant.c:1
platform_thread_t video_capture_thread
Video capture thread handle.

References ASCIICHAT_OK, session_participant::connected, session_participant::enable_video, errno_checkpoint(), ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_THREAD, GET_OPTION, session_participant::initialized, log_error, log_info, session_capture_config_t::loop, MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_WEBCAM, session_capture_config_t::path, safe_snprintf(), session_av_state_begin(), session_av_state_complete(), session_av_state_is_active(), session_capture_create(), session_participant_stop_video(), SESSION_PARTICIPANT_WEBCAM_INDEX_BUFFER_SIZE, SET_ERRNO, session_capture_config_t::type, session_participant::video, session_participant::video_capture, session_participant::video_capture_thread, and session_participant::video_thread_created.

◆ session_participant_stop_audio()

void session_participant_stop_audio ( session_participant_t * p)

#include <src/common/session/participant.h>

Stop audio capture and streaming.

Parameters
pParticipant handle (must not be NULL)

Definition at line 695 of file participant.c.

695 {
696 if (!p || !p->initialized)
697 return;
699 if (p->audio_thread_created) {
700 if (thread_join(&p->audio_capture_thread, NULL) != 0) {
701 log_error("Failed to join audio capture worker; retaining its resources");
702 return;
703 }
704 p->audio_thread_created = false;
705 }
706 if (p->audio_capture) {
709 p->audio_capture = NULL;
710 }
711 if (p->opus_encoder) {
713 p->opus_encoder = NULL;
714 }
715 p->audio_uses_video = false;
717}
void session_av_state_finish_stop(session_av_state_t *state)
Definition av_state.c:57
void session_av_state_begin_stop(session_av_state_t *state)
Definition av_state.c:52
void opus_codec_destroy(opus_codec_t *codec)
Destroy an Opus codec instance.
Definition opus.c:280

References session_participant::audio, session_participant::audio_capture, session_participant::audio_capture_thread, session_participant::audio_thread_created, session_participant::audio_uses_video, session_participant::initialized, log_error, opus_codec_destroy(), session_participant::opus_encoder, session_audio_destroy(), session_av_state_begin_stop(), session_av_state_finish_stop(), session_capture_set_audio_context(), and session_participant::video_capture.

Referenced by session_participant_start_audio(), and session_participant_stop_video().

◆ session_participant_stop_video()

void session_participant_stop_video ( session_participant_t * p)

#include <src/common/session/participant.h>

Stop and join video capture, then release its resources Also stops audio when it may borrow the video media source.

Parameters
pParticipant handle (must not be NULL)

Definition at line 603 of file participant.c.

603 {
604 if (!p || !p->initialized)
605 return;
606 if (p->audio_uses_video)
609 return;
611 if (p->video_thread_created) {
612 if (thread_join(&p->video_capture_thread, NULL) != 0) {
613 log_error("Failed to join video capture worker; retaining its resources");
614 return;
615 }
616 p->video_thread_created = false;
617 }
618 if (p->video_capture) {
620 p->video_capture = NULL;
621 }
623}

References session_participant::audio_thread_created, session_participant::audio_uses_video, session_participant::initialized, log_error, session_av_state_begin_stop(), session_av_state_finish_stop(), session_capture_destroy(), session_participant_stop_audio(), session_participant::video, session_participant::video_capture, session_participant::video_capture_thread, and session_participant::video_thread_created.

Referenced by session_participant_start_video().

◆ session_render_loop()

asciichat_error_t session_render_loop ( session_capture_ctx_t * capture,
session_display_ctx_t * display,
session_should_exit_fn should_exit,
session_capture_fn capture_cb,
session_sleep_for_frame_fn sleep_cb,
session_keyboard_handler_fn keyboard_handler,
void * user_data )

#include <src/common/session/render.h>

Unified render loop for all display modes.

Flexible render loop that supports both synchronous and event-driven modes. Optionally integrates keyboard input handling for interactive controls.

Synchronous Mode (capture != NULL, callbacks == NULL): Handles complete render lifecycle:

Used by: mirror mode, discovery participant mode

Event-Driven Mode (capture == NULL, callbacks != NULL): Delegates all frame operations to callbacks:

Used by: client mode, async frame sources

Keyboard Support:

  • If keyboard_handler is provided, initializes keyboard input at loop start
  • Polls for keyboard input each frame iteration
  • Calls keyboard_handler callback when keys are pressed
  • Cleans up keyboard on loop exit
  • Safe to pass NULL for no keyboard support (backward compatible)
Parameters
captureCapture context for synchronous mode (NULL if using callbacks)
displayDisplay context (must not be NULL)
should_exitCallback to check exit condition each iteration (must not be NULL)
capture_cbCustom capture callback for event-driven mode (NULL if using capture context)
sleep_cbCustom sleep callback for event-driven mode (NULL if using capture context)
keyboard_handlerOptional keyboard handler callback (NULL for no keyboard support)
user_dataOpaque pointer passed to all callbacks
Returns
ASCIICHAT_OK on success, error code on failure
Note
Either capture XOR (capture_cb && sleep_cb) must be provided, not both
Caller must free capture and display contexts after this returns
In synchronous mode, capture callback can return NULL on frame unavailability
In event-driven mode, capture callback can return NULL gracefully
keyboard_handler is optional - pass NULL to disable keyboard support
Synchronous Example (Mirror Mode)
static bool my_should_exit(void *user_data) {
return atomic_load(&g_exit_flag);
}
int main(void) {
platform_set_console_ctrl_handler(my_ctrl_handler);
capture, display, my_should_exit,
NULL, NULL, // No custom callbacks
NULL // No user_data needed
);
return result == ASCIICHAT_OK ? 0 : (int)result;
}
Event-Driven Example (Client Mode)
static image_t *client_capture_frame(void *user_data) {
client_ctx_t *ctx = (client_ctx_t *)user_data;
return network_frame_queue_pop_nonblocking(ctx->frame_queue);
}
static void client_sleep_for_frame(void *user_data) {
client_ctx_t *ctx = (client_ctx_t *)user_data;
if (network_frame_queue_empty(ctx->frame_queue)) {
}
}
static bool client_should_exit(void *user_data) {
}
int main(void) {
client_ctx_t client_ctx = {...};
NULL, display, client_should_exit, // NULL capture = event-driven
client_capture_frame, // Custom capture
client_sleep_for_frame, // Custom sleep
&client_ctx // Context for callbacks
);
return result == ASCIICHAT_OK ? 0 : (int)result;
}
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
bool server_connection_is_active(client_connection_t *connection)
Establish connection to ascii-chat server.

Definition at line 60 of file src/common/session/render.c.

63 {
64 // Validate required parameters
65 log_debug("capture=%p, display=%p, mode=%s", (void *)capture, (void *)display,
66 capture ? "SYNCHRONOUS" : "EVENT-DRIVEN");
67
68 if (!display) {
69 SET_ERRNO(ERROR_INVALID_PARAM, "display context is NULL");
71 }
72
73 if (!should_exit) {
74 SET_ERRNO(ERROR_INVALID_PARAM, "should_exit callback is NULL");
76 }
77
78 // Validate mode: either capture context OR custom callbacks, not both
79 if (!capture && !capture_cb) {
80 SET_ERRNO(ERROR_INVALID_PARAM, "must provide either capture context or capture callback");
82 }
83
84 if (capture && capture_cb) {
85 SET_ERRNO(ERROR_INVALID_PARAM, "cannot provide both capture context and capture callback");
87 }
88
89 // In event-driven mode, both capture_cb and sleep_cb must be provided together
90 if (capture_cb && !sleep_cb) {
91 SET_ERRNO(ERROR_INVALID_PARAM, "capture_cb requires sleep_cb (must provide both or neither)");
93 }
94
95 if (sleep_cb && !capture_cb) {
96 SET_ERRNO(ERROR_INVALID_PARAM, "sleep_cb requires capture_cb (must provide both or neither)");
98 }
99
100 // SYNCHRONOUS MODE: Use threaded pipeline for capture + render
101 if (capture != NULL) {
102 log_debug("[SESSION_RENDER_LOOP_SYNCHRONOUS] Creating pipeline... (capture=%p, display=%p)", (void *)capture,
103 (void *)display);
104 session_pipeline_t *pipeline = NULL;
105 asciichat_error_t pipeline_err = session_pipeline_create(capture, display, &pipeline);
106 log_debug("[SESSION_RENDER_LOOP_SYNCHRONOUS] pipeline_create returned: %d (pipeline=%p)", pipeline_err,
107 (void *)pipeline);
108 if (pipeline_err != ASCIICHAT_OK) {
109 log_error("[SESSION_RENDER_LOOP_SYNCHRONOUS] Pipeline creation failed %s",
110 asciichat_error_format(NULL, pipeline_err));
111 return pipeline_err;
112 }
113 log_debug("[SESSION_RENDER_LOOP_SYNCHRONOUS] Pipeline created successfully, running main loop...");
114
115 // Disable logging to stdout/stderr during rendering (same as event-driven mode).
116 // This prevents logs from mixing with ASCII art output. Logs still go to file.
118 asciichat_error_t run_err = session_pipeline_run_main(pipeline, should_exit, keyboard_handler, user_data);
120
121 asciichat_error_t drain_err = session_pipeline_destroy(pipeline);
122 // The encoder no longer writes here. Use capture duration, excluding time spent draining.
123 if (GET_OPTION(snapshot_mode) && display && g_snapshot_first_capture_ns > 0)
125
126 return run_err == ASCIICHAT_OK ? drain_err : run_err;
127 } else {
128 log_warn("capture is NULL - will not use pipeline (capture_cb=%p)", (void *)capture_cb);
129 }
130
131 // Snapshot mode state tracking
132 bool snapshot_mode = GET_OPTION(snapshot_mode);
133 bool snapshot_done = false;
134 uint64_t frames_rendered_since_first = 0; // Track frames rendered after first frame (for snapshot delay)
135 // NOTE: g_snapshot_first_frame_rendered is set by display.c when first frame is rendered via platform_write_all()
136
137 log_debug("display=%p capture=%p capture_cb=%p snapshot_mode=%s snapshot_delay=%.2f", (void *)display,
138 (void *)capture, (void *)capture_cb, snapshot_mode ? "YES" : "NO",
139 snapshot_mode ? GET_OPTION(snapshot_delay) : 0.0);
140 if (should_exit(user_data)) {
141 log_warn("EXIT signal already set at entry, aborting render loop");
142 return ASCIICHAT_OK;
143 }
144
145 // Keyboard input initialization (if keyboard handler is provided)
146 // Disable keyboard only in snapshot mode
147 // Keyboard is pre-initialized from asciichat_shared_init()
148 // Allow keyboard input in all other modes if handler provided
149 bool keyboard_enabled = keyboard_handler && !snapshot_mode;
150
151 // Pause mode state tracking (for pause/unpause transitions in event-driven mode)
152 bool initial_paused_frame_rendered = false;
153 bool is_paused = false;
154 log_debug("Keyboard setup - handler=%p snapshot=%s enabled=%s", (void *)keyboard_handler,
155 snapshot_mode ? "YES" : "NO", keyboard_enabled ? "YES" : "NO");
156
157 // Log TTY detection status for debugging keyboard issues
158 log_debug("TTY Status: stdin_tty=%d stdout_tty=%d interactive=%d keyboard_enabled=%d", terminal_is_stdin_tty(),
159 terminal_is_stdout_tty(), terminal_is_interactive(), keyboard_enabled);
160
161 // Frame rate timing (event-driven mode only)
162 uint64_t frame_count = 0;
163 uint64_t frame_start_ns = 0;
164
165 // Event-driven mode: disable console logging during rendering to prevent logs from corrupting frame display
166 // This must be done BEFORE the render loop to avoid repeated lock/unlock cycles that could cause deadlocks
167 log_debug("[EVENT_DRIVEN_MODE] Entering render loop. snapshot_mode=%s", snapshot_mode ? "YES" : "NO");
169
170 asciichat_error_t capture_result = ASCIICHAT_OK;
171 // Main render loop
172 int loop_iteration = 0;
173 log_debug("[RENDER_LOOP_START] snapshot_mode=%s, snapshot_delay=%.2f", snapshot_mode ? "YES" : "NO",
174 snapshot_mode ? GET_OPTION(snapshot_delay) : 0.0);
175
176 // Wait for splash animation to complete before rendering first frame
177 // This prevents ASCII art from mixing with splash screen output
178 // But skip for immediate snapshots (snapshot-delay=0) which need to output first frame immediately
179 static bool splash_wait_done = false;
180 if (!splash_wait_done) {
181 splash_wait_done = true;
182 if (!(GET_OPTION(snapshot_mode) && GET_OPTION(snapshot_delay) == 0.0)) {
183 // Re-enable logging briefly for splash wait, then disable again
187 }
188 }
189
190 // In snapshot mode, we must render at least one frame before checking should_exit
191 // to ensure the snapshot delay timer can start
192 bool force_first_iteration = snapshot_mode && frame_count == 0;
193
194 // Register display context globally so signal handlers (Ctrl+C) can access it
195 // This enables Ctrl+C to close the help screen instead of quitting when help is active
197
198 while (force_first_iteration || (!should_exit(user_data) && !(snapshot_mode && snapshot_done))) {
199 force_first_iteration = false;
200 loop_iteration++;
201 log_debug("[LOOP_ITER] iteration=%d, snapshot_done=%s, frame_count=%lu", loop_iteration,
202 snapshot_done ? "YES" : "NO", frame_count);
203 if (loop_iteration % 60 == 0) {
204 log_debug("iteration %d, should_exit check returning false", loop_iteration);
205 }
206 // Snapshot mode: exit at start of iteration if done
207 // This prevents frame 2+ from being captured when snapshot_delay has elapsed
208 if (snapshot_mode && snapshot_done && frame_count > 0) {
209 log_debug("[SNAPSHOT_EXIT] Snapshot mode: exiting at loop iteration start (snapshot_done=true, frames=%lu)",
210 frame_count);
211 break;
212 }
213
214 log_debug_every(US_PER_SEC_INT, "frame %lu", frame_count);
215 // Frame timing - measure total time to maintain target FPS
216 frame_start_ns = time_get_ns();
217
218 // Frame capture and timing - mode-dependent
219 image_t *image = NULL;
220 uint64_t capture_start_ns = 0;
221 uint64_t capture_end_ns = 0;
222 uint64_t pre_convert_ns = 0;
223 uint64_t post_convert_ns = 0;
224 uint64_t conversion_elapsed_ns = 0;
225
226 // EVENT-DRIVEN MODE: Frames come from callbacks
227 // Both sleep_cb and capture_cb are guaranteed non-NULL by validation above
228 sleep_cb(user_data);
229 ASCII_OTEL_SCOPE(PIPELINE_DISPLAY);
231 image = capture_cb(user_data);
232
233 if (!image) {
235 if (errno_peek_since(frame_scope, &failure)) {
236 capture_result = failure.code;
237 break;
238 }
239 // No frame available - this is normal in async modes (network latency, etc.)
240 // Just continue to next iteration, don't exit
241 continue;
242 }
243
244 // Event-driven mode: increment frame count
245 frame_count++;
246
247 // Convert image to ASCII using display context
248 // Handles all palette, terminal caps, width, height, stretch settings
249 pre_convert_ns = time_get_ns();
250 char *ascii_frame = session_display_convert_to_ascii_reuse(display, image);
251 post_convert_ns = time_get_ns();
252 conversion_elapsed_ns = post_convert_ns - pre_convert_ns;
253
254 if (frame_count <= 3) {
255 log_debug("RENDER_LOOP[%lu]: image=%p, ascii_frame=%p (conversion took %u ms)", frame_count, (void *)image,
256 (void *)ascii_frame, (unsigned)(conversion_elapsed_ns / NS_PER_MS_INT));
257 }
258
259 // Declare variables that need scope beyond the if (ascii_frame) block
260 bool is_paused_frame = initial_paused_frame_rendered && is_paused;
261 bool output_paused_frame = snapshot_mode && is_paused_frame;
262 uint64_t pre_render_ns = 0, post_render_ns = 0;
263
264 if (ascii_frame) {
265 log_debug_every(US_PER_SEC_INT, "ascii_frame ready (len=%zu)", strlen(ascii_frame));
266
267 // Always attempt to render frames; the display context will handle filtering based on:
268 // - TTY mode: render all frames with cursor control (even in snapshot mode, for animation)
269 // - Piped mode: render all frames without cursor control (for continuous capture)
270 // - Snapshot mode on non-TTY: only the display context renders the final frame
271 // BUT: don't render if splash screen is still animating - wait for it to finish
272 bool splash_running = splash_is_running();
273 bool should_write = !splash_running;
274 if (frame_count == 1) {
275 log_dev("[RENDER_FRAME] Frame 1: splash_is_running=%s, should_write=%s", splash_running ? "YES" : "NO",
276 should_write ? "YES" : "NO");
277 }
278 if (should_write) {
279 // is_final = true when: snapshot done, or paused frame (for both snapshot and pause modes)
280 // Profile: render frame
281 pre_render_ns = time_get_ns();
282
283 if (frame_count <= 5 || frame_count % 10 == 0) {
284 log_debug("Frame %lu - calling session_display_render_frame, display=%p", (unsigned long)frame_count,
285 (void *)display);
286 }
287
288 // Check if SIGINT handler cancelled help mode (Ctrl+C while help is open).
289 // The signal handler sets an atomic flag instead of touching state directly.
290 if (display && keyboard_help_check_signal_cancel()) {
291 keyboard_help_toggle(display);
292 log_debug("Ctrl+C cancelled help screen - returning to ASCII art");
293 }
294
295 session_display_render_frame(display, ascii_frame);
296
297 // Snapshot mode: increment frame counter for all displayed frames
298 // Counter starts at 1 for the first displayed frame, then 2, 3, 4... for subsequent frames
299 // Timer is started by display.c when g_snapshot_first_frame_rendered is set
300 if (snapshot_mode && g_snapshot_first_frame_rendered) {
301 frames_rendered_since_first++;
302 log_debug_every(US_PER_SEC_INT, "[SNAPSHOT] Frame rendered: frames_rendered_since_first=%lu",
303 frames_rendered_since_first);
304 }
305 }
306
307 // Keyboard input polling (if enabled) - MUST come before snapshot exit check so help screen can be toggled
308 // Allow keyboard in snapshot mode too (for help screen toggle debugging)
309 // Only enable keyboard if BOTH stdin AND stdout are TTYs to avoid buffering issues
310 // when tcsetattr() modifies the tty line discipline
311 if (keyboard_enabled && keyboard_handler) {
312 uint64_t keyboard_start_ns = time_get_ns();
314 double keyboard_elapsed_ns = (double)time_elapsed_ns(keyboard_start_ns, time_get_ns());
315 if (keyboard_elapsed_ns >= 0.0) {
316 char _duration_str[TIME_DURATION_BUFFER_SIZE];
317 time_pretty((uint64_t)keyboard_elapsed_ns, -1, _duration_str, sizeof(_duration_str));
318 log_dev_every(1 * NS_PER_SEC_INT, "RENDER[%lu] Keyboard read complete (key=%d) in %s",
319 (unsigned long)frame_count, key, _duration_str);
320 }
321 if (key != KEY_NONE) {
322 log_debug("KEYBOARD: Key pressed: code=%d char='%c'", key, (key >= 32 && key < 127) ? key : '?');
323 // Normal keyboard handler
324 log_debug("KEYBOARD: Normal handler taking key %d", key);
325 keyboard_handler(capture, key, user_data);
326 }
327 }
328
329 // Free frame before checking exit conditions to avoid double-free
330 session_display_release_ascii(display, ascii_frame);
331 }
332
333 // Snapshot mode: check if elapsed time has reached snapshot_delay duration (terminal render timing)
334 // Separate from capture timing - both should measure their own snapshot_delay
335 if (snapshot_mode && !snapshot_done) {
337 double snapshot_delay = GET_OPTION(snapshot_delay);
338 uint64_t now_ns = time_get_ns();
339 uint64_t elapsed_ns = now_ns - g_snapshot_first_frame_rendered_ns;
340 double elapsed_sec = (double)elapsed_ns / (double)NS_PER_SEC_INT;
341
342 if (frames_rendered_since_first == 1 || frames_rendered_since_first % 20 == 0) {
343 log_debug("SNAPSHOT: RENDER_CHECK iter=%lu elapsed=%.3f target=%.2f (from first_frame_rendered)",
344 frames_rendered_since_first, elapsed_sec, snapshot_delay);
345 }
346
347 // snapshot_delay=0 means exit after first frame
348 // snapshot_delay>0 means wait that many seconds before exiting
349 // Exit is based on elapsed wall-clock time, not frame count
350 bool should_exit = (snapshot_delay == 0.0) || (elapsed_sec >= snapshot_delay);
351
352 if (should_exit) {
353 log_debug("SNAPSHOT: RENDER_DONE at iteration %lu - elapsed=%.3f target=%.2f (setting snapshot_done=true)",
354 frames_rendered_since_first, elapsed_sec, snapshot_delay);
355 // We don't end frames with newlines so the next log would print on the same line as the frame's
356 // last row without an \n here. We only need this \n in stdout when interactive,
357 // so piped snapshots don't have a weird newline in stdout that they don't need.
358 if (terminal_is_interactive()) {
359 ui_controller_printf(STDOUT_FILENO, "\n");
360 }
361 snapshot_done = true;
362 }
363 } else {
364 if (frames_rendered_since_first == 0) {
365 log_debug("SNAPSHOT: Waiting for first_frame_rendered (g_snapshot_first_frame_rendered=%d, "
366 "g_snapshot_first_frame_rendered_ns=%llu)",
368 }
369 }
370 }
371
372 // Exit conditions: snapshot mode exits after capturing the final frame or initial paused frame
373 if (snapshot_mode && (snapshot_done || output_paused_frame)) {
374 log_debug("SNAPSHOT: EXIT CONDITION MET - snapshot_done=%d, output_paused_frame=%d", snapshot_done,
375 output_paused_frame);
376
377 // Calculate elapsed time from first frame captured (for video file)
378 // This is separate from render time - video should span from first capture to last capture
379 double capture_elapsed_sec = 0.0;
381 uint64_t now_ns = time_get_ns();
382 uint64_t elapsed_ns = now_ns - g_snapshot_first_capture_ns;
383 capture_elapsed_sec = (double)elapsed_ns / (double)NS_PER_SEC_INT;
384 log_debug("SNAPSHOT: EXIT - Capture elapsed: %.3f seconds, render elapsed: %.3f seconds, "
385 "g_snapshot_first_capture_ns=%llu",
386 capture_elapsed_sec,
389 : 0.0,
390 (unsigned long long)g_snapshot_first_capture_ns);
391 } else {
392 log_warn("SNAPSHOT: EXIT condition met but g_snapshot_first_capture_ns is 0!");
393 }
394
395 // Pass actual capture elapsed time to encoder for correct video duration
396 log_debug("SNAPSHOT: About to call session_display_set_snapshot_actual_duration with duration=%.3f, display=%p",
397 capture_elapsed_sec, (void *)display);
398 if (display) {
399 session_display_set_snapshot_actual_duration(display, capture_elapsed_sec);
400 } else {
401 log_warn("SNAPSHOT: display context is NULL!");
402 }
403
404 // Signal application to exit in snapshot mode
406 break;
407 }
408
409 // Frame rendering and timing details (moved down since snapshot exit is now above it)
410 if (ascii_frame) {
411
412 // Measure frame completion right after rendering, BEFORE keyboard polling
413 // This gives us accurate timing for just the core frame operations
414 uint64_t frame_end_render_ns = time_get_ns();
415
416 // Calculate each phase duration
417 uint64_t prestart_ms =
418 (capture_start_ns > frame_start_ns) ? (capture_start_ns - frame_start_ns) / NS_PER_MS_INT : 0;
419 uint64_t capture_ms =
420 (capture_end_ns > capture_start_ns) ? (capture_end_ns - capture_start_ns) / NS_PER_MS_INT : 0;
421 uint64_t convert_ms = conversion_elapsed_ns / NS_PER_MS_INT;
422 uint64_t render_ms =
423 (post_render_ns > pre_render_ns && post_render_ns > 0) ? (post_render_ns - pre_render_ns) / NS_PER_MS_INT : 0;
424 uint64_t total_ms =
425 (frame_end_render_ns > frame_start_ns) ? (frame_end_render_ns - frame_start_ns) / NS_PER_MS_INT : 0;
426
427 // Log phase breakdown every 5 frames
428 if (frame_count % 5 == 0) {
430 2 * NS_PER_SEC_INT,
431 "PHASE_BREAKDOWN[%lu]: prestart=%llu ms, capture=%llu ms, convert=%llu ms, render=%llu ms (total=%llu ms)",
432 frame_count, prestart_ms, capture_ms, convert_ms, render_ms, total_ms);
433 }
434 } else {
435 // Snapshot mode: even if frame conversion failed, check if we should exit
436 // This ensures snapshot_delay is honored even if display context isn't rendering
437 if (snapshot_mode && snapshot_done) {
438 break;
439 }
440 }
441
442 // In event-driven mode, frame rate limiting is handled by the sleep_cb callback
443 // Synchronous mode (with capture context) uses the pipeline and returns early above
444
445 // Note: Images returned by media sources are cached/reused and should NOT be destroyed
446 // The image pointers are managed by the source and will be cleaned up on source shutdown
447 } // while (!should_exit(user_data)) {
448
449 // Re-enable console logging after rendering completes
451 if (!snapshot_mode && terminal_is_interactive()) {
452 ui_controller_printf(STDOUT_FILENO, "\n");
453 }
454
455 // Keyboard input cleanup (if it was initialized)
456 if (keyboard_enabled) {
459 log_debug_every(2 * NS_PER_SEC_INT, "Keyboard input disabled");
460 }
461
462 // Unregister global display context (for signal handlers)
464
465 return capture_result;
466}
#define APP_CALLBACK_VOID(callback_name)
#define log_dev(...)
Log a DEV message (most verbose, development only).
Definition log/log.h:581
void log_set_terminal_output(bool enabled)
Control stderr output to terminal.
Definition log.c:746
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as a compact duration with configurable sub-minute precision.
Definition time.c:374
#define TIME_DURATION_BUFFER_SIZE
Definition time.h:40
keyboard_key_t
Options for interactive line editing.
Definition keyboard.h:53
@ KEY_NONE
No key pressed or no input available.
Definition keyboard.h:54
void keyboard_help_toggle(session_display_ctx_t *ctx)
Toggle keyboard help on/off.
bool splash_is_running(void)
Check if splash screen animation is currently running.
Definition splash.c:445
void session_display_set_snapshot_actual_duration(session_display_ctx_t *ctx, double actual_duration_sec)
Set the actual wall-clock duration for snapshot mode frame timing.
bool keyboard_help_check_signal_cancel(void)
Check if help screen closure was requested by signal handler.
void splash_wait_for_animation(void)
Wait for splash animation thread to fully exit.
Definition splash.c:451
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:746
void signal_exit(void)
Definition main.c:253
bool should_exit(void)
Definition main.c:249
asciichat_error_t session_pipeline_run_main(session_pipeline_t *pipeline, session_should_exit_fn should_exit, session_keyboard_handler_fn keyboard_handler, void *user_data)
Definition pipeline.c:630
asciichat_error_t session_pipeline_create(session_capture_ctx_t *capture, session_display_ctx_t *display, session_pipeline_t **out)
Definition pipeline.c:541
bool g_snapshot_first_frame_rendered
asciichat_error_t session_pipeline_destroy(session_pipeline_t *pipeline)
Definition pipeline.c:749
struct session_pipeline_s session_pipeline_t
Definition pipeline.h:22
void session_display_release_ascii(session_display_ctx_t *ctx, char *ascii)
char * session_display_convert_to_ascii_reuse(session_display_ctx_t *ctx, const image_t *image)
uint64_t g_snapshot_first_frame_rendered_ns
uint64_t g_snapshot_first_capture_ns
uint64_t g_snapshot_actual_duration_ms
asciichat_error_t ui_controller_printf(int fd, const char *format,...)
keyboard_key_t ui_input_read_key(ui_screen_t screen)
Definition tui/input.c:353

References APP_CALLBACK_VOID, ASCII_OTEL_SCOPE, asciichat_error_format(), ASCIICHAT_OK, asciichat_error_context_t::code, errno_checkpoint(), errno_peek_since(), ERROR_INVALID_PARAM, g_snapshot_actual_duration_ms, g_snapshot_first_capture_ns, g_snapshot_first_frame_rendered, g_snapshot_first_frame_rendered_ns, GET_OPTION, KEY_NONE, keyboard_help_check_signal_cancel(), keyboard_help_toggle(), log_debug, log_debug_every, log_dev, log_dev_every, log_error, log_set_terminal_output(), log_warn, NS_PER_MS_INT, NS_PER_SEC_INT, session_display_convert_to_ascii_reuse(), session_display_release_ascii(), session_display_render_frame(), session_display_set_global_context(), session_display_set_snapshot_actual_duration(), session_pipeline_create(), session_pipeline_destroy(), session_pipeline_run_main(), SET_ERRNO, should_exit(), signal_exit(), splash_is_running(), splash_wait_for_animation(), TIME_DURATION_BUFFER_SIZE, time_elapsed_ns(), time_pretty(), ui_controller_printf(), ui_controller_remove(), ui_input_read_key(), UI_SCREEN_HELP, UI_SCREEN_MEDIA, and US_PER_SEC_INT.

◆ splash_clear_display_context()

void splash_clear_display_context ( void )

#include <include/ascii-chat/tui/splash.h>

Clear the cached display context to prevent use-after-free.

Must be called when the display context is being destroyed to prevent worker threads from accessing freed memory. This clears the reference stored in g_splash_state.

Note
Thread-safe (uses atomic operations)
Safe to call multiple times

Definition at line 441 of file splash.c.

441 {
442 // No-op: display context is no longer cached
443}

Referenced by session_display_destroy().

◆ splash_intro_done()

int splash_intro_done ( void )

#include <include/ascii-chat/tui/splash.h>

Signal that intro splash should end (first frame ready to render).

Tells the splash screen animation to stop displaying. On the next animation frame attempt, it will clear the screen and exit cleanly.

Important: Call splash_wait_for_animation() after this to ensure the animation thread has fully exited before rendering ASCII art.

Returns
ASCIICHAT_OK on success
Note
Safe to call multiple times
After calling this, next frame render should show real ASCII art
The animation thread checks this flag and exits gracefully
MUST call splash_wait_for_animation() before displaying ASCII frames

Definition at line 426 of file splash.c.

426 {
427 atomic_store_u64(&g_splash_state.intro_done_time_ns, time_get_ns());
428 if (discovery_splash_snapshot().enabled || (GET_OPTION(snapshot_mode) && GET_OPTION(snapshot_delay) == 0.0))
429 atomic_store_bool(&g_splash_state.should_stop, true);
430 // The monitor retires the splash snapshot before clearing is_running. Keep
431 // thread_created set until splash_wait_for_animation() joins that monitor.
432
433 return 0;
434}
bool enabled
Is filtering active?
Definition grep.c:62

References enabled, and GET_OPTION.

Referenced by display_disable_logging_for_first_frame(), session_connection_establish(), and session_display_write_ascii_frame().

◆ splash_intro_start()

int splash_intro_start ( session_display_ctx_t * ctx)

#include <include/ascii-chat/tui/splash.h>

Start animated intro screen with rainbow ASCII art (non-blocking).

Starts the splash screen animation that runs while initialization happens in the background. The animation continues until splash_intro_done() is called.

Display Requirements:

  • stdin AND stdout must be TTY (platform_isatty())
  • –splash-screen=false must not be set
  • Not in snapshot mode (–snapshot)
  • Startup layout needs 50 columns x 20 rows; discovery uses a compact layout below that.

Behavior:

  • Starts animated rainbow border with "ascii-chat" logo
  • Runs continuously until splash_intro_done() is called
  • Animation loops with rainbow color cycling
  • When splash_intro_done() called, animation stops on next frame
  • Next real frame render clears screen and shows ASCII art
Parameters
ctxDisplay context (can be NULL for minimal stdout-only mode)
Returns
ASCIICHAT_OK on success, error code on failure
Note
Non-blocking - returns immediately after starting animation
Call splash_intro_done() when first frame is ready to render
Uses lock-free option access via GET_OPTION() macro

Definition at line 375 of file splash.c.

375 {
376 (void)ctx; // Not needed - display context is not actively used
377
378 // Check if splash should display
379 if (!splash_should_display(true)) {
380 return 0;
381 }
382
383 discovery_splash_t discovery = discovery_splash_snapshot();
384 if (discovery.enabled && (!terminal_is_interactive() || !terminal_is_stdout_tty() || GET_OPTION(snapshot_mode))) {
385 return 0;
386 }
387 if (atomic_load_bool(&g_splash_state.thread_created))
388 return 0;
389
390 // Redirect splash screen to stderr when stdout is piped (not a TTY).
391 // This keeps stdout clean for frame data while splash goes to stderr.
392 // Uses platform_isatty() so non-desktop platforms (iOS, WASM) can override.
393 if (!platform_isatty(STDOUT_FILENO)) {
394 terminal_screen_set_output_fd(STDERR_FILENO);
395 }
396
397 // Initialize log buffer (same pattern as server_status)
399
400 // Clear log buffer for clean slate
402
403 // Set running flag
404 atomic_store_bool(&g_splash_state.is_running, true);
405 atomic_store_bool(&g_splash_state.should_stop, false);
406 atomic_store_u64(&g_splash_state.intro_done_time_ns, 0);
407 g_splash_state.start_time_ns = time_get_ns();
408
409 // Start monitor thread (only create once)
410 bool expected_false = false;
411 if (!atomic_cas_bool(&g_splash_state.thread_created, &expected_false, true)) {
412 return 0;
413 }
414
415 int err = platform_thread_create(&g_splash_state.monitor_thread, "splash_monitor", splash_monitor_thread, NULL);
416 if (err != ASCIICHAT_OK) {
417 log_warn("Failed to create splash monitor thread: error=%d", err);
418 atomic_store_bool(&g_splash_state.thread_created, false);
419 atomic_store_bool(&g_splash_state.is_running, false);
420 return 0;
421 }
422
423 return 0; // ASCIICHAT_OK
424}
void splash_log_init(void)
Initialize log buffer for splash animation.
Definition splash.c:124
void splash_log_clear(void)
Clear splash log buffer.
Definition splash.c:136
bool splash_should_display(bool is_intro)
Render splash header with rainbow animation (callback for terminal_screen).
Definition splash.c:242
ui_discovery_view_t discovery_splash_t
Definition splash.c:70
void terminal_screen_set_output_fd(int fd)
Set output file descriptor for terminal screens (splash/status).

References ASCIICHAT_OK, ui_discovery_view_t::enabled, GET_OPTION, log_warn, splash_log_clear(), splash_log_init(), splash_should_display(), and terminal_screen_set_output_fd().

◆ splash_is_running()

bool splash_is_running ( void )

#include <include/ascii-chat/tui/splash.h>

Check if splash screen animation is currently running.

Returns true if the splash animation thread is actively rendering. Used to prevent ASCII art frame rendering from overlapping with the splash.

Returns
true if splash is running, false otherwise
Note
Thread-safe (uses atomic operations)
Can be called from render loop to skip frame output during splash

Definition at line 445 of file splash.c.

445 {
446 // Check if splash animation is currently rendering
447 // Used to prevent ASCII art from rendering while splash is animating
448 return atomic_load_bool(&g_splash_state.is_running);
449}

Referenced by session_render_loop().

◆ splash_log_append()

void splash_log_append ( const char * message)

#include <include/ascii-chat/tui/splash.h>

Append message to splash log buffer.

Delegates to the standard terminal_screen log append. Called by logging system to capture messages for display.

Parameters
messageLog message to append

◆ splash_log_clear()

void splash_log_clear ( void )

#include <include/ascii-chat/tui/splash.h>

Clear splash log buffer.

Delegates to the standard terminal_screen log clear. Useful when starting fresh splash animation.

Definition at line 136 of file splash.c.

136 {
138}
void terminal_screen_log_clear(void)
Clear buffered logs for terminal screens.

References terminal_screen_log_clear().

Referenced by splash_intro_start().

◆ splash_log_destroy()

void splash_log_destroy ( void )

#include <include/ascii-chat/tui/splash.h>

Destroy splash log buffer.

Delegates to the standard terminal_screen log cleanup.

Definition at line 131 of file splash.c.

131 {
134}
void log_clear_session_log_buffer(void)
Unregister the session log buffer.
Definition log.c:2057
void terminal_screen_log_destroy(void)
Standard log cleanup for terminal screens.

References log_clear_session_log_buffer(), and terminal_screen_log_destroy().

◆ splash_log_init()

void splash_log_init ( void )

#include <include/ascii-chat/tui/splash.h>

Initialize log buffer for splash animation.

Delegates to the standard terminal_screen log initialization. Call once at startup before rendering splash screen.

Definition at line 124 of file splash.c.

124 {
126 if (buf) {
128 }
129}
struct session_log_buffer session_log_buffer_t
Thread-safe fixed-size history ring for session-screen log lines.
void log_set_session_log_buffer(session_log_buffer_t *buf)
Register a session log buffer with the logger.
Definition log.c:2048
session_log_buffer_t * terminal_screen_log_init(void)
Initialize session log buffer for terminal screens.

References log_set_session_log_buffer(), and terminal_screen_log_init().

Referenced by splash_intro_start().

◆ splash_notify_first_frame()

void splash_notify_first_frame ( void )

#include <include/ascii-chat/tui/splash.h>

Notify splash that the first frame has been rendered.

Called from display_render_frame() when the first ASCII frame is rendered. This updates the splash state's atomic flag so the splash animation knows when to stop.

Important: This function does NOT access the display context pointer, making it safe to call from any thread even if the context is being freed.

Note
Thread-safe (uses atomic operations)
Safe to call multiple times
Must NOT dereference display_ctx (unlike splash_intro_done)

◆ splash_restore_stderr()

void splash_restore_stderr ( void )

#include <include/ascii-chat/tui/splash.h>

Restore stderr after splash animation completes.

Restores the original stderr file descriptor that was saved when splash_intro_start() was called. This must be called after splash animation completes to allow debug logging to appear on screen again.

Note
Safe to call multiple times (idempotent)
Must only be called after splash_intro_start() was called

Definition at line 436 of file splash.c.

436 {
437 // No-op: stderr suppression is no longer used
438 // Logging works naturally through session_log_buffer
439}

◆ splash_set_update_notification()

void splash_set_update_notification ( const char * notification)

#include <include/ascii-chat/tui/splash.h>

Set update notification to display on splash/status screens.

Sets a notification message that will be displayed on splash screens (intro and status) to inform users about available updates.

Parameters
notificationUpdate notification message (can be NULL or empty to clear)
Note
This should be called before splash_intro_start()
Thread-safe via internal mutex

Definition at line 481 of file splash.c.

481 {
482 splash_mutex_init(&g_update_notification_lifecycle, &g_update_notification_mutex, "update_notification");
483 mutex_lock(&g_update_notification_mutex);
484
485 if (!notification || notification[0] == '\0') {
486 g_update_notification[0] = '\0';
487 log_debug("Cleared update notification for splash/status screens");
488 } else {
489 SAFE_STRNCPY(g_update_notification, notification, sizeof(g_update_notification));
490 log_debug("Set update notification for splash/status screens: %s", notification);
491 }
492
493 mutex_unlock(&g_update_notification_mutex);
494}

References log_debug, and SAFE_STRNCPY.

◆ splash_should_display()

bool splash_should_display ( bool is_intro)

#include <include/ascii-chat/tui/splash.h>

Check if splash screen should display.

Determines if TTY checks pass and options allow splash display. Respects both TTY detection and option flags.

Parameters
is_introtrue for intro screen checks, false for status screen
Returns
true if splash should display, false if should skip

Check if splash screen should display.

Renders exactly 8 lines:

  • Line 1: Top border
  • Lines 2-5: ASCII logo (4 lines, centered, rainbow animated)
  • Line 6: Tagline (centered)
  • Line 7: Connection target (centered)
  • Line 8: Bottom border
Parameters
bufFrame buffer to write into
term_sizeCurrent terminal dimensions
user_dataPointer to splash_header_ctx_t

Definition at line 242 of file splash.c.

242 {
243 // Check option flags (display splash if enabled, regardless of TTY for testing)
244 if (is_intro) {
245 // Allow splash in snapshot mode if loading from URL/file (has loading time)
246 // Skip splash only for quick webcam snapshots (unless explicitly enabled)
247 bool splash_screen_opt = GET_OPTION(splash_screen);
248 bool splash_explicitly_set = GET_OPTION(splash_screen_explicitly_set);
249 bool is_snapshot = GET_OPTION(snapshot_mode);
250 bool has_media = (GET_OPTION(media_url) && strlen(GET_OPTION(media_url)) > 0) ||
251 (GET_OPTION(media_file) && strlen(GET_OPTION(media_file)) > 0);
252
253 // Show splash if enabled, but in snapshot mode only if:
254 // 1. Has media file/URL, OR
255 // 2. Explicitly enabled with --splash-screen=true
256 bool should_display = splash_screen_opt && (!is_snapshot || has_media || splash_explicitly_set);
257 // Show splash if:
258 // 1. Not in snapshot mode, OR
259 // 2. In snapshot mode but loading from URL/file (needs splash during load)
260 return should_display;
261 } else {
262 return GET_OPTION(status_screen);
263 }
264}

References GET_OPTION.

Referenced by splash_intro_start().

◆ splash_wait_for_animation()

void splash_wait_for_animation ( void )

#include <include/ascii-chat/tui/splash.h>

Wait for splash animation thread to fully exit.

Blocks until the splash animation thread has completed and exited. Must be called after splash_intro_done() and before rendering ASCII frames to prevent the splash and ASCII art from appearing simultaneously.

Note
Safe to call multiple times
On shutdown, waits at most 100ms for a blocked writer before returning. Normal frame handoff always waits for the writer to finish.
MUST be called before first display_render_frame() call

Definition at line 451 of file splash.c.

451 {
452 if (!atomic_load_bool(&g_splash_state.thread_created))
453 return;
454
455 // Keep terminal ownership until the writer finishes, but allow shutdown to
456 // proceed if an undrained output pipe blocks the monitor thread. Polling
457 // also handles shutdown requested after the normal handoff has begun, and
458 // avoids platforms whose timed thread join falls back to a blocking join.
459 uint64_t shutdown_started_ns = 0;
460 while (atomic_load_bool(&g_splash_state.is_running)) {
461 if (shutdown_is_requested()) {
462 atomic_store_bool(&g_splash_state.should_stop, true);
463 uint64_t now = time_get_ns();
464 if (shutdown_started_ns == 0)
465 shutdown_started_ns = now;
466 if (now - shutdown_started_ns >= 100 * NS_PER_MS_INT)
467 return;
468 }
470 }
471
472 // The monitor clears is_running after retiring its presentation snapshot.
473 asciichat_error_t err = thread_join(&g_splash_state.monitor_thread, NULL);
474 if (err != ASCIICHAT_OK) {
475 log_warn("Splash animation join failed: %s", asciichat_error_format(NULL, err));
476 return;
477 }
478 atomic_store_bool(&g_splash_state.thread_created, false);
479}
bool shutdown_is_requested(void)
Check if shutdown has been requested.
Definition common.c:86

References asciichat_error_format(), ASCIICHAT_OK, log_warn, NS_PER_MS_INT, platform_sleep_ms(), and shutdown_is_requested().

Referenced by session_connection_establish(), session_display_write_ascii_frame(), and session_render_loop().

◆ update_banner_cancel_check()

void update_banner_cancel_check ( void )

#include <include/ascii-chat/tui/update_banner.h>

Cancel an in-flight background startup check before joining its worker.

Definition at line 750 of file update_banner.c.

750 {
751 atomic_store(&g_update_cancel_requested, true);
752}

◆ update_banner_handle_key()

bool update_banner_handle_key ( int key)

#include <include/ascii-chat/tui/update_banner.h>

Consumes F12 and overlay-local keys before mode-specific shortcut handling.

Definition at line 682 of file update_banner.c.

682 {
683 if (key == KEY_F12) {
685 return true;
686 }
687 if (!g_update_overlay)
688 return false;
689 bool checking = update_banner_is_checking();
690 update_check_result_t result = {0};
691 bool available = !checking && update_banner_copy_result(&result) && result.update_available;
692 int count = update_action_count(false, checking, available);
693 if (update_select_key(key, count))
694 return true;
695 if (key == '\r' || key == '\n') {
696 int selected = atomic_load(&g_update_selected_action) % count;
697 if ((available && selected == 2) || (!available && selected == 0)) {
699 return true;
700 }
701 if (checking)
702 return true;
703 if (available && selected == 0) {
705 return true;
706 }
707 if (available && selected == 1) {
708 update_banner_open_release();
709 return true;
710 }
711 key = 'r';
712 }
713 if (key == KEY_ESCAPE) {
715 return true;
716 }
717 if (key == 'o' || key == 'O') {
719 update_banner_open_release();
720 return true;
721 }
722 if (key == 'r' || key == 'R') {
724 return true;
726 update_banner_start_check_impl(true);
727 return true;
728 }
729 return g_update_overlay;
730}
@ KEY_F12
Open the version update checker TUI.
Definition keyboard.h:69
bool update_banner_install_update(void)
void update_banner_wait_for_check(void)
Wait for the background update check thread to finish.
bool update_banner_is_checking(void)
void update_banner_toggle_overlay(void)
update check result keeps the values that move through the subsystem: update_available,...
bool update_available
True if newer version exists.

References KEY_ESCAPE, KEY_F12, update_check_result_t::update_available, update_banner_install_update(), update_banner_is_checking(), update_banner_toggle_overlay(), and update_banner_wait_for_check().

Referenced by ui_input_read_key().

◆ update_banner_has_update()

bool update_banner_has_update ( void )

#include <include/ascii-chat/tui/update_banner.h>

Check if an update is available (thread-safe).

Returns
true if update_banner_set_result() was called with an available update

Definition at line 110 of file update_banner.c.

110 {
111 update_check_result_t result = {0};
112 return update_banner_copy_result(&result) && result.update_available;
113}

References update_check_result_t::update_available.

◆ update_banner_install_update()

bool update_banner_install_update ( void )

#include <include/ascii-chat/tui/update_banner.h>

Run the installation command associated with this binary and request exit on success.

Definition at line 362 of file update_banner.c.

362 {
363 update_check_result_t result = {0};
364 if (!update_banner_copy_result(&result) || !result.update_available) {
365 return false;
366 }
367 g_update_install_failed = false;
368
370 int exit_code = -1;
371#ifndef NDEBUG
372 const char *test_command = SAFE_GETENV("ASCII_CHAT_TEST_UPDATE_COMMAND");
373 if (test_command && test_command[0]) {
374#ifdef _WIN32
375 const char *argv[] = {"cmd", "/d", "/c", test_command, NULL};
376 exit_code = platform_execute_subprocess("cmd", argv, NULL, 0, NULL, NULL);
377#else
378 const char *argv[] = {"/bin/sh", "-c", test_command, NULL};
379 exit_code = platform_execute_subprocess("/bin/sh", argv, NULL, 0, NULL, NULL);
380#endif
381 } else
382#endif
383 if (method == INSTALL_METHOD_HOMEBREW) {
384 const char *argv[] = {"brew", "upgrade", "ascii-chat", NULL};
385 exit_code = platform_execute_subprocess("brew", argv, NULL, 0, NULL, NULL);
386 } else if (method == INSTALL_METHOD_SCOOP) {
387 // Scoop is a PowerShell script/shim, rather than a native executable.
388 const char *argv[] = {"powershell", "-NoProfile", "-Command",
389 "$ErrorActionPreference = 'Stop'; scoop update ascii-chat; if (!$?) { exit 1 }", NULL};
390 exit_code = platform_execute_subprocess("powershell", argv, NULL, 0, NULL, NULL);
391 } else if (method == INSTALL_METHOD_ARCH_AUR) {
392 const char *helper = update_check_get_aur_helper();
393 if (!helper) {
394 log_warn("No supported AUR helper is available");
395 g_update_install_failed = true;
397 return false;
398 }
399 const char *argv[] = {helper, "-S", "ascii-chat", NULL};
400 exit_code = platform_execute_subprocess(helper, argv, NULL, 0, NULL, NULL);
401 } else {
402#ifdef _WIN32
403 const char *argv[] = {
404 "powershell", "-NoProfile", "-ExecutionPolicy",
405 "Bypass", "-Command", "$ErrorActionPreference = 'Stop'; irm https://ascii-chat.com/install.ps1 | iex",
406 NULL};
407 exit_code = platform_execute_subprocess("powershell", argv, NULL, 0, NULL, NULL);
408#else
409 const char *argv[] = {"bash", "-o", "pipefail", "-c", "curl -fsSL https://ascii-chat.com/install.sh | bash", NULL};
410 exit_code = platform_execute_subprocess("bash", argv, NULL, 0, NULL, NULL);
411#endif
412 }
413 if (exit_code != 0) {
414 log_warn("ascii-chat update command exited with status %d", exit_code);
415 g_update_install_failed = true;
417 return false;
418 }
422 return true;
423}
#define SAFE_GETENV(name)
Definition common.h:449
install_method_t
Installation method for suggesting upgrade command.
const char * update_check_get_aur_helper(void)
install_method_t update_check_detect_install_method(void)
Detect installation method.
@ INSTALL_METHOD_ARCH_AUR
Installed via Arch AUR (paru/yay).
@ INSTALL_METHOD_SCOOP
Installed via Scoop.
@ INSTALL_METHOD_HOMEBREW
Installed via Homebrew.
int platform_execute_subprocess(const char *executable, const char **argv, char *output_buffer, size_t output_size, bool(*cancel)(void *), void *data)
Execute a subprocess and optionally capture its output.
void update_banner_print_instructions(void)
Print upgrade instructions to stdout and prepare for clean exit.
void shutdown_request(void)
Definition common.c:82

References APP_CALLBACK_VOID, INSTALL_METHOD_ARCH_AUR, INSTALL_METHOD_HOMEBREW, INSTALL_METHOD_SCOOP, log_warn, platform_execute_subprocess(), SAFE_GETENV, shutdown_request(), signal_exit(), ui_controller_redraw(), update_check_result_t::update_available, update_banner_print_instructions(), update_check_detect_install_method(), and update_check_get_aur_helper().

Referenced by update_banner_handle_key().

◆ update_banner_is_checking()

bool update_banner_is_checking ( void )

#include <include/ascii-chat/tui/update_banner.h>

True while the shared startup or explicit update request is in flight.

Definition at line 642 of file update_banner.c.

642 {
643 if (!lifecycle_is_initialized(&g_update_result_lifecycle))
644 return false;
645 mutex_lock(&g_update_result_mutex);
646 bool checking = g_update_checking;
647 mutex_unlock(&g_update_result_mutex);
648 return checking;
649}

Referenced by ui_session_splash_render(), and update_banner_handle_key().

◆ update_banner_is_overlay_open()

bool update_banner_is_overlay_open ( void )

#include <include/ascii-chat/tui/update_banner.h>

True only while the F12 overlay, rather than the startup prompt, owns the update screen.

Definition at line 651 of file update_banner.c.

651 {
652 return g_update_overlay;
653}

Referenced by ui_input_read_key().

◆ update_banner_print_instructions()

void update_banner_print_instructions ( void )

#include <include/ascii-chat/tui/update_banner.h>

Print upgrade instructions to stdout and prepare for clean exit.

Called after update_banner_show_prompt() returns true. Prints a clear, copy-pasteable upgrade command to stdout.

Definition at line 339 of file update_banner.c.

339 {
340 mutex_lock(&g_update_result_mutex);
342 memcpy(&result, &g_update_result, sizeof(result));
343 mutex_unlock(&g_update_result_mutex);
344
347 (void)terminal_format_hyperlink(domain_link, sizeof(domain_link), "https://ascii-chat.com", "ascii-chat.com",
348 !GET_OPTION(strip_ansi));
349 snprintf(instructions, sizeof(instructions), "Full instructions on %s", domain_link);
351 int len = snprintf(output, sizeof(output),
352 "\033[0m\033[2J\033[H\033[?25h"
353 "\nUpdated ascii-chat: %s -> %s\n\nRun ascii-chat again to start the new version.\n\n"
354 "Binaries for every OS with release notes on GitHub:\n%s\n\n"
355 "%s\n\n",
356 result.current_version, result.latest_version, result.release_url,
357 colored_string(LOG_COLOR_WHITE, instructions));
358 if (len > 0 && len < (int)sizeof(output))
359 ui_controller_finish(STDOUT_FILENO, output, (size_t)len);
360}
@ LOG_COLOR_WHITE
Definition log/log.h:156
const char * colored_string(log_color_t color, const char *text)
Build a colored string for terminal output.
Definition string.c:284
char latest_version[64]
Latest version tag (e.g., "v0.9.0").
char current_version[64]
Current version string.
char release_url[512]
GitHub release page URL.
void ui_controller_finish(int fd, const char *data, size_t len)
#define TUI_UPDATE_BANNER_DOMAIN_LINK_BUFFER_SIZE
#define TUI_UPDATE_BANNER_OUTPUT_BUFFER_SIZE
#define TUI_UPDATE_BANNER_INSTRUCTIONS_EXTENDED_BUFFER_SIZE

References colored_string(), update_check_result_t::current_version, GET_OPTION, update_check_result_t::latest_version, LOG_COLOR_WHITE, update_check_result_t::release_url, TUI_UPDATE_BANNER_DOMAIN_LINK_BUFFER_SIZE, TUI_UPDATE_BANNER_INSTRUCTIONS_EXTENDED_BUFFER_SIZE, TUI_UPDATE_BANNER_OUTPUT_BUFFER_SIZE, and ui_controller_finish().

Referenced by update_banner_install_update().

◆ update_banner_set_result()

void update_banner_set_result ( const update_check_result_t * result)

#include <include/ascii-chat/tui/update_banner.h>

Store the update check result (thread-safe).

Called from the background update check thread after a successful check that found an available update. The result is stored under mutex protection.

Parameters
resultUpdate check result to store (copied internally)

Definition at line 89 of file update_banner.c.

89 {
90 if (!result) {
91 return;
92 }
93
94 // Resolve the command before publishing a result to the presentation thread.
97
98 if (lifecycle_init_once(&g_update_result_lifecycle)) {
99 mutex_init(&g_update_result_mutex, "update_banner_result");
100 lifecycle_init_commit(&g_update_result_lifecycle);
101 }
102
103 ascii_otel_ui_usage("update", result->check_succeeded ? "check_completed" : "check_failed", 0);
104 mutex_lock(&g_update_result_mutex);
105 memcpy(&g_update_result, result, sizeof(g_update_result));
106 g_update_result_set = true;
107 mutex_unlock(&g_update_result_mutex);
108}
bool check_succeeded
True if check completed (even if no update).

References ascii_otel_ui_usage(), update_check_result_t::check_succeeded, INSTALL_METHOD_ARCH_AUR, update_check_result_t::update_available, update_check_detect_install_method(), and update_check_get_aur_helper().

Referenced by update_banner_show_prompt().

◆ update_banner_show_prompt()

bool update_banner_show_prompt ( struct session_display_ctx * ctx)

#include <include/ascii-chat/tui/update_banner.h>

Show the update prompt screen and wait for user input.

Renders a centered box with version info, upgrade command, and release URL. Blocks until the user presses a key:

  • Y/y/Enter: returns true (user wants to exit and update)
  • N/n/Esc: returns false (user wants to continue)
Parameters
ctxDisplay context for terminal output
Returns
true if user chose to update, false if user chose to continue

Definition at line 287 of file update_banner.c.

287 {
288 if (!ctx || !terminal_can_prompt_user(!GET_OPTION(snapshot_mode)))
289 return false;
291 mutex_lock(&g_update_result_mutex);
292 result = g_update_result;
293 mutex_unlock(&g_update_result_mutex);
294 if (update_check_is_dismissed(&result)) {
295 return false;
296 }
297 atomic_store(&g_update_selected_action, 0);
298 if (ui_controller_submit(UI_SCREEN_UPDATE, STDOUT_FILENO, (terminal_size_t){.cols = 32, .rows = 23},
299 render_update_prompt, &result, sizeof(result)) != ASCIICHAT_OK)
300 return false;
301 bool update = false;
302 while (!shutdown_is_requested()) {
303 /* The update prompt is synchronous, but a failed DNS check can put the
304 * error popup above it. Route keys to that modal until it is dismissed. */
307 continue;
308 }
310 if (g_update_overlay)
311 return false;
312 if (update_select_key(key, 3))
313 continue;
314 if (key == 'o' || key == 'O' || ((key == '\r' || key == '\n') && atomic_load(&g_update_selected_action) == 1)) {
315 update_banner_open_release();
316 continue;
317 }
318 if (key == '\r' || key == '\n') {
319 update = atomic_load(&g_update_selected_action) == 0;
320 break;
321 }
322 if (key == 'y' || key == 'Y') {
323 update = true;
324 break;
325 }
326 if (key == 'n' || key == 'N' || key == KEY_ESCAPE)
327 break;
328 }
329 if (!g_update_overlay) {
331 }
332 if (!update && !g_update_overlay) {
335 }
336 return update;
337}
bool ui_error_popup_visible(void)
Definition error_popup.c:71
void update_check_dismiss_for_a_week(update_check_result_t *result)
bool update_check_is_dismissed(const update_check_result_t *result)
void update_banner_set_result(const update_check_result_t *result)
Store the update check result (thread-safe).
keyboard_key_t ui_input_wait_key(ui_screen_t screen, unsigned timeout_ms)
Definition tui/input.c:554

References ASCIICHAT_OK, GET_OPTION, KEY_ESCAPE, shutdown_is_requested(), ui_controller_remove(), ui_controller_submit(), ui_error_popup_visible(), ui_input_wait_key(), UI_SCREEN_ERROR, UI_SCREEN_UPDATE, update_banner_set_result(), update_check_dismiss_for_a_week(), and update_check_is_dismissed().

◆ update_banner_start_check()

void update_banner_start_check ( void )

#include <include/ascii-chat/tui/update_banner.h>

Start the background update check thread.

Spawns a background thread that performs the update check (DNS + HTTPS) and stores the result. The splash screen notification is also set if an update is found. Call update_banner_wait_for_check() later to join.

Definition at line 603 of file update_banner.c.

603 {
604 update_banner_start_check_impl(false);
605}

Referenced by main().

◆ update_banner_toggle_overlay()

void update_banner_toggle_overlay ( void )

#include <include/ascii-chat/tui/update_banner.h>

Toggle the retained update overlay. This is safe from every mode's shared input path.

Definition at line 607 of file update_banner.c.

607 {
608 if (g_update_overlay) {
609 ascii_otel_ui_usage("update", "dismissed", 0);
610 g_update_overlay = false;
612 return;
613 }
614 ascii_otel_ui_usage("update", "opened", 0);
615 g_update_overlay = true;
616 atomic_store(&g_update_visible_until_ns, time_get_ns() + 2 * NS_PER_SEC_INT);
617 atomic_store(&g_update_selected_action, 0);
618 bool finished = false;
619 if (lifecycle_is_initialized(&g_update_result_lifecycle)) {
620 mutex_lock(&g_update_result_mutex);
621 finished = g_update_finished;
622 mutex_unlock(&g_update_result_mutex);
623 }
624 if (g_update_thread_started && finished)
626 if (!g_update_thread_started)
627 update_banner_start_check_impl(true);
628 update_banner_present_overlay();
629}

References ascii_otel_ui_usage(), NS_PER_SEC_INT, ui_controller_remove(), UI_SCREEN_UPDATE, and update_banner_wait_for_check().

Referenced by ui_input_read_key(), and update_banner_handle_key().

◆ update_banner_wait_for_check()

void update_banner_wait_for_check ( void )

#include <include/ascii-chat/tui/update_banner.h>

Wait for the background update check thread to finish.

Blocks up to 5 seconds for the background check to complete. Safe to call even if no check was started.

Definition at line 732 of file update_banner.c.

732 {
733 if (!g_update_thread_started) {
734 return;
735 }
736 // Shutdown must remain responsive after cancellation. The worker checks
737 // g_update_cancel_requested and terminates its child process, so one second
738 // is sufficient for normal teardown while avoiding a multi-second Ctrl-C
739 // stall when a platform/network call is slow to unwind.
740 int ret = thread_join_timeout(&g_update_thread, NULL, 1LL * NS_PER_SEC_INT);
741 if (ret != 0) {
742 // Timeout โ€” thread is still alive. Do a blocking join to avoid orphaning it.
743 // The thread has bounded runtime (DNS + HTTP both have timeouts) so this won't hang.
744 log_warn("Update check thread did not finish within 1s, waiting for completion");
745 thread_join(&g_update_thread, NULL);
746 }
747 g_update_thread_started = false;
748}

References log_warn, and NS_PER_SEC_INT.

Referenced by update_banner_handle_key(), and update_banner_toggle_overlay().