ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
Session Management Module

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

Files

file  fps_counter.c
 FPS counter implementation using rolling-window timestamp buffer.
 
file  keyboard_help.c
 Keyboard help overlay TUI rendering implementation.
 
file  splash.c
 Intro and status screen display implementation with animated rainbow effects.
 
file  update_banner.c
 Update available prompt screen implementation.
 
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  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  settings.c
 ⚙️ Session settings serialization implementation
 
file  settings.h
 ⚙️ Session settings serialization and synchronization
 
file  session_log_buffer.h
 Thread-safe circular log buffer for session screens (status, splash)
 
file  fps_counter.h
 ⏱️ FPS counter for measuring output write throughput
 
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.
 

Data Structures

struct  session_capture_config_t
 Configuration for session capture context. More...
 
struct  session_display_config_t
 Configuration for session display context. More...
 
struct  session_host_client_info_t
 Client information structure. More...
 
struct  session_host_callbacks_t
 Callback function prototypes for session host events. More...
 
struct  session_host_config_t
 Configuration for session host. More...
 
struct  session_participant_callbacks_t
 Callback function prototypes for session participant events. More...
 
struct  session_participant_config_t
 Configuration for session participant. More...
 
struct  session_settings_t
 Session settings for transmission between peers. More...
 

Macros

#define SESSION_SETTINGS_VERSION   1
 Current session settings structure version.
 
#define SESSION_SETTINGS_SERIALIZED_SIZE   64
 Size of serialized session settings in bytes.
 

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
 Opaque session capture context handle.
 
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
 Opaque session display context handle.
 
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
 
typedef struct session_participant session_participant_t
 Opaque session participant handle.
 
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 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)
 
void session_display_destroy (session_display_ctx_t *ctx)
 Destroy session display context and free resources.
 
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)
 
void 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_render_fps_overlay (session_display_ctx_t *ctx)
 Render FPS counter overlay in top-right corner.
 
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.
 
void 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.
 
void 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_client (session_host_t *host, socket_t socket, const char *ip, int port)
 Add a client from an accepted socket.
 
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 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)
 Set an alternative 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 alternative 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)
 Create a new session 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 video capture and streaming.
 
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 session settings update (if permitted)
 
asciichat_error_t session_participant_start_video_capture (session_participant_t *p)
 Start video capture and transmission to host.
 
void session_participant_stop_video_capture (session_participant_t *p)
 Stop video capture and transmission.
 
asciichat_error_t session_participant_start_audio_capture (session_participant_t *p)
 Start audio capture and transmission to host.
 
void session_participant_stop_audio_capture (session_participant_t *p)
 Stop audio capture and transmission.
 
socket_t session_participant_get_socket (session_participant_t *p)
 Get the socket from a participant context.
 
asciichat_error_t session_participant_set_transport (session_participant_t *p, acip_transport_t *transport)
 Set an alternative transport 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 an active alternative 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 session_settings_init (session_settings_t *settings)
 Initialize session settings to defaults.
 
asciichat_error_t session_settings_serialize (const session_settings_t *settings, uint8_t *buffer, size_t *len)
 Serialize session settings to binary buffer.
 
asciichat_error_t session_settings_deserialize (const uint8_t *buffer, size_t len, session_settings_t *settings)
 Deserialize session settings from binary buffer.
 
asciichat_error_t session_settings_from_options (session_settings_t *settings)
 Populate settings from current global options.
 
asciichat_error_t session_settings_apply_to_options (const session_settings_t *settings)
 Apply settings to global options.
 
bool session_settings_needs_update (uint32_t local_version, uint32_t remote_version)
 Check if settings need update based on versions.
 
bool session_settings_equal (const session_settings_t *a, const session_settings_t *b)
 Compare two settings structures for equality.
 
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 keyboard_help_toggle (session_display_ctx_t *ctx)
 Toggle keyboard help on/off.
 
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.
 
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.
 

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:

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 │
│ ──────────── ────────── ────────── │
│ - 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) │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Internal session host structure.
Definition host.c:106
Internal session participant structure.
Definition participant.c:45

Session State Machines

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

Host State Machine

┌─────────────────────────────────────────────────────────────┐
│ SESSION HOST LIFECYCLE │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ │
│ v │
│ ┌──────────────┐ │
│ │ CREATED │ - Allocated but not listening │
│ │ │ - No network socket yet │
│ │ │ - Config applied, ready for start │
│ └──────────────┘ │
│ │ │
│ v │
│ │ │
│ 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 │
│ │ │
│ v │
│ ┌──────────────┐ │
│ │ STOPPED │ - Socket closed, no new connections │
│ │ │ - Existing clients still drain buffers │
│ │ │ - Ready for destroy │
│ └──────────────┘ │
│ │ │
│ v │
│ │ │
│ v │
│ ┌──────────────┐ │
│ │ DESTROYED │ - All resources freed │
│ │ │ - Handle invalid │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
void session_host_stop(session_host_t *host)
Stop accepting connections and disconnect all clients.
Definition host.c:1086
asciichat_error_t session_host_start(session_host_t *host)
Start accepting client connections.
Definition host.c:1026
void session_host_destroy(session_host_t *host)
Destroy session host and free resources.
Definition host.c:249
session_host_t * session_host_create(const session_host_config_t *config)
Create a new session host.
Definition host.c:193
bool initialized
Definition mmap.c:38

Participant State Machine

┌─────────────────────────────────────────────────────────────┐
│ SESSION PARTICIPANT LIFECYCLE │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ │
│ v │
│ ┌──────────────┐ │
│ │ CREATED │ - Allocated but not connected │
│ │ │ - Config applied, no network socket │
│ └──────────────┘ │
│ │ │
│ v │
│ │ │
│ 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 │
│ └──────────────┘ │
│ │ │
│ │ │ │
│ │ v │
│ │ ┌──────────────────┐ │
│ │ │ VIDEO STREAMING │ - Sending local frames │
│ │ │ │ - on_frame_received fired │
│ │ └──────────────────┘ │
│ │ │
│ │ │ │
│ │ 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 │
│ │ │
│ 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)
Create a new session 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 │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ │
│ 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 │
│ │ │
│ v │
│ ┌──────────────┐ │
│ │ DESTROYED │ - Media source closed │
│ │ │ - FFmpeg subprocess terminated │
│ │ │ - Handle invalid │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
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.
int frame
Definition splash.c:99

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:472

Synchronization Primitives

RCU (Read-Copy-Update) for Settings:

Circular Buffer for Logs:

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):

File Source (FFmpeg):

URL Source (FFmpeg with yt-dlp resolution):

Test Pattern (Procedural):

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:

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.
Image structure.

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
├─ 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

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:

Memory Ownership

Clear ownership prevents double-frees and memory leaks:

session_client_like_run owns and manages:

Mode callbacks receive:

Framebuffer Policy:

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:

All sources provide a unified interface:

Terminal Display

The display abstraction (session_display_ctx_t) manages terminal rendering:

Render Loop

The unified render loop (session_render_loop) abstracts two patterns:

Synchronous Mode (with capture context):

Event-Driven Mode (with custom callbacks):

Audio Handling

Audio operations are decoupled from media source:

Keyboard Input

Interactive controls via keyboard (session_handle_keyboard_input):

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:

Manages:

Consensus Protocol

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

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:1630
asciichat_error_t session_host_start_render(session_host_t *host)
Start media rendering thread (video mixing and audio distribution)
Definition host.c:1571
Configuration for session host.
Definition host.h:154
int port
Port to listen on (default: 27224)
Definition host.h:156

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
Configuration for session participant.

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
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ MEDIA_SOURCE_WEBCAM
Hardware webcam device.
Definition source.h:82

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(var)
Check if an error occurred and get full context.
@ ASCIICHAT_OK
Definition error_codes.h:51
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
Error context structure.
char * context_message
Optional custom message (dynamically allocated, owned by system)

Thread Safety

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:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
unsigned char uint8_t
Definition common.h:56

Encryption and Authentication

All connections are encrypted by default using:

Supported authentication modes:

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:168
const char * key_path
Path to server identity key.
Definition host.h:171
const char * password
Password for client authentication (optional)
Definition host.h:174

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

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:

USAGE:

// Create audio context for a participant
// Start capture and playback
// Cleanup
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.
Internal session audio context structure.
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:

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
Configuration for session capture context.
Internal session capture context structure.
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:

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:88
void session_display_destroy(session_display_ctx_t *ctx)
Destroy session display context and free resources.
session_display_ctx_t * session_display_create(const session_display_config_t *config)
Create a new session display context.
void session_display_render_frame(session_display_ctx_t *ctx, const char *frame_data)
Render an ASCII frame to the terminal.
Configuration for session display context.
Internal session display context structure.
@ TERM_COLOR_AUTO
Auto-detect color support from terminal capabilities.
Definition terminal.h:580
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:

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:58
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

Provides keyboard input handling for session-level controls including:

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:

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,
.callbacks = { .on_connected = on_connected, .on_frame_received = on_frame }
};
// Start streaming
// Cleanup
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

This header provides settings structures and serialization functions for session configuration that can be transmitted between peers in discovery mode.

CORE FEATURES:

USAGE:

// Create settings from current options
// Serialize for transmission
size_t len;
session_settings_serialize(&settings, buffer, &len);
// Deserialize received settings
session_settings_deserialize(buffer, len, &received);
// Check if update needed
if (session_settings_needs_update(local_version, received.version)) {
}
asciichat_error_t session_settings_deserialize(const uint8_t *buffer, size_t len, session_settings_t *settings)
Deserialize session settings from binary buffer.
Definition settings.c:101
asciichat_error_t session_settings_apply_to_options(const session_settings_t *settings)
Apply settings to global options.
Definition settings.c:198
bool session_settings_needs_update(uint32_t local_version, uint32_t remote_version)
Check if settings need update based on versions.
Definition settings.c:219
asciichat_error_t session_settings_from_options(session_settings_t *settings)
Populate settings from current global options.
Definition settings.c:160
asciichat_error_t session_settings_serialize(const session_settings_t *settings, uint8_t *buffer, size_t *len)
Serialize session settings to binary buffer.
Definition settings.c:51
#define SESSION_SETTINGS_SERIALIZED_SIZE
Size of serialized session settings in bytes.
Definition settings.h:59
Session settings for transmission between peers.
Definition settings.h:77
uint32_t version
Settings version for conflict detection (monotonically increasing)
Definition settings.h:79
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

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.
Definition fps_counter.c:74
void fps_counter_tick(fps_counter_t *counter)
Record a frame timestamp.
Definition fps_counter.c:57
void fps_counter_destroy(fps_counter_t *counter)
Destroy FPS counter and free resources.
Definition fps_counter.c:47
fps_counter_t * fps_counter_create(void)
Create a new FPS counter.
Definition fps_counter.c:33
FPS counter state.
Definition fps_counter.c:23
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
February 2026

Provides an interactive keyboard help overlay that displays:

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

Display State:

Threading:

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.

Macro Definition Documentation

◆ SESSION_SETTINGS_SERIALIZED_SIZE

#define SESSION_SETTINGS_SERIALIZED_SIZE   64

#include <settings.h>

Size of serialized session settings in bytes.

Definition at line 59 of file settings.h.

◆ SESSION_SETTINGS_VERSION

#define SESSION_SETTINGS_VERSION   1

#include <settings.h>

Current session settings structure version.

Definition at line 56 of file settings.h.

Typedef Documentation

◆ acip_transport_t [1/2]

#include <host.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 474 of file host.h.

◆ acip_transport_t [2/2]

#include <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 435 of file participant.h.

◆ fps_counter_t

typedef struct fps_counter_s fps_counter_t

#include <fps_counter.h>

Opaque FPS counter handle.

Maintains a rolling window of frame timestamps for FPS calculation.

Definition at line 39 of file fps_counter.h.

◆ session_audio_ctx_t

#include <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 <capture.h>

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 127 of file common/session/capture.h.

◆ session_capture_fn

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

#include <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 common/session/render.h.

◆ session_capture_should_exit_fn

typedef bool(* session_capture_should_exit_fn) (void *user_data)

#include <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/2]

#include <display.h>

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 120 of file src/common/session/display.h.

◆ session_display_ctx_t [2/2]

#include <keyboard_handler.h>

Definition at line 24 of file keyboard_handler.h.

◆ session_display_should_exit_fn

typedef bool(* session_display_should_exit_fn) (void *user_data)

#include <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 <host.h>

Opaque session host handle.

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

Definition at line 71 of file host.h.

◆ session_keyboard_handler_fn

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

#include <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 common/session/render.h.

◆ session_participant_t

#include <participant.h>

Opaque session participant handle.

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

Definition at line 71 of file participant.h.

◆ session_should_exit_fn

typedef bool(* session_should_exit_fn) (void *user_data)

#include <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 common/session/render.h.

◆ session_sleep_for_frame_fn

typedef void(* session_sleep_for_frame_fn) (void *user_data)

#include <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 common/session/render.h.

Function Documentation

◆ fps_counter_create()

fps_counter_t * fps_counter_create ( void  )

#include <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 33 of file fps_counter.c.

33 {
34 fps_counter_t *counter = SAFE_CALLOC(1, sizeof(fps_counter_t), fps_counter_t *);
35 if (!counter) {
36 return NULL;
37 }
38
39 // Initialize circular buffer pointers
40 counter->head = 0;
41 counter->count = 0;
42 memset(counter->frame_times, 0, sizeof(counter->frame_times));
43
44 return counter;
45}
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
int count
Number of valid entries in buffer (0 to FPS_WINDOW_SIZE)
Definition fps_counter.c:26
int head
Current write position (0 to FPS_WINDOW_SIZE-1)
Definition fps_counter.c:25
uint64_t frame_times[30]
Circular buffer of timestamps (ns)
Definition fps_counter.c:24

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

Referenced by session_display_create().

◆ fps_counter_destroy()

void fps_counter_destroy ( fps_counter_t *  counter)

#include <fps_counter.h>

Destroy FPS counter and free resources.

Parameters
counterCounter to destroy (can be NULL)

Definition at line 47 of file fps_counter.c.

47 {
48 if (counter) {
49 SAFE_FREE(counter);
50 }
51}

References SAFE_FREE.

Referenced by session_display_destroy().

◆ fps_counter_get()

float fps_counter_get ( fps_counter_t *  counter)

#include <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.

Definition at line 74 of file fps_counter.c.

74 {
75 if (!counter || counter->count < 2) {
76 return 0.0f;
77 }
78
79 // Find indices of oldest and newest timestamps in the buffer
80 int oldest_idx = (counter->head - counter->count + FPS_WINDOW_SIZE) % FPS_WINDOW_SIZE;
81 int newest_idx = (counter->head - 1 + FPS_WINDOW_SIZE) % FPS_WINDOW_SIZE;
82
83 // Get the elapsed time between oldest and newest frames
84 uint64_t oldest_time = counter->frame_times[oldest_idx];
85 uint64_t newest_time = counter->frame_times[newest_idx];
86 uint64_t elapsed_ns = newest_time - oldest_time;
87
88 // Avoid division by zero
89 if (elapsed_ns == 0) {
90 return 0.0f;
91 }
92
93 // FPS = (number of frames) / elapsed_time
94 // We have (count - 1) frames across the elapsed time
95 // (count - 1) frames means count timestamps, so count-1 intervals
96 return (float)(counter->count - 1) * 1e9f / (float)elapsed_ns;
97}
#define FPS_WINDOW_SIZE
Number of frames to average over.
Definition fps_counter.c:15
unsigned long long uint64_t
Definition common.h:59

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

Referenced by session_display_render_fps_overlay().

◆ fps_counter_tick()

void fps_counter_tick ( fps_counter_t *  counter)

#include <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 57 of file fps_counter.c.

57 {
58 if (!counter) {
59 return;
60 }
61
62 // Record current time at head position
63 counter->frame_times[counter->head] = time_get_ns();
64
65 // Advance head pointer with wraparound
66 counter->head = (counter->head + 1) % FPS_WINDOW_SIZE;
67
68 // Track how many valid entries we have (up to FPS_WINDOW_SIZE)
69 if (counter->count < FPS_WINDOW_SIZE) {
70 counter->count++;
71 }
72}
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108

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

Referenced by session_display_write_ascii().

◆ keyboard_help_check_signal_cancel()

bool keyboard_help_check_signal_cancel ( void  )

#include <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 1206 of file src/common/session/display.c.

1206 {
1207 bool expected = true;
1208 return atomic_cas_bool(&g_help_signal_cancel, &expected, false);
1209}
bool atomic_cas_bool(atomic_t *a, bool *expected, bool new_value)
Atomically compare-and-swap a boolean.
Definition atomic.c:184

References atomic_cas_bool().

Referenced by session_render_loop().

◆ keyboard_help_is_active()

bool keyboard_help_is_active ( session_display_ctx_t *  ctx)

#include <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 1211 of file src/common/session/display.c.

1211 {
1212 if (!ctx) {
1213 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL");
1214 return false;
1215 }
1216
1217 // Disable help in snapshot mode only - allow help state check even if stdin/stdout aren't TTYs
1218 if (GET_OPTION(snapshot_mode)) {
1219 return false;
1220 }
1221
1223}
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
@ ERROR_INVALID_PARAM
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
atomic_t keyboard_help_active
Keyboard help active flag (toggled with '?') - atomic for thread-safe access.

References atomic_load_bool(), ERROR_INVALID_PARAM, GET_OPTION, session_display_ctx::keyboard_help_active, and SET_ERRNO.

Referenced by display_render_frame(), session_handle_keyboard_input(), session_pipeline_run_main(), and session_render_loop().

◆ keyboard_help_is_active_global()

bool keyboard_help_is_active_global ( void  )

#include <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 1228 of file src/common/session/display.c.

1228 {
1229 session_display_ctx_t *ctx = get_current_display_ctx();
1230 if (!ctx) {
1231 platform_write_all(STDERR_FILENO, "[DEBUG: ctx is NULL]\n", 20);
1232 return false;
1233 }
1234 bool active = atomic_load_bool(&ctx->keyboard_help_active);
1235 const char *msg = active ? "[DEBUG: help active]\n" : "[DEBUG: help inactive]\n";
1236 platform_write_all(STDERR_FILENO, msg, active ? 22 : 24);
1237 return active;
1238}
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all bytes to a file descriptor, handling partial writes.
Definition system.c:224

References atomic_load_bool(), session_display_ctx::keyboard_help_active, and platform_write_all().

◆ keyboard_help_render()

void keyboard_help_render ( session_display_ctx_t *  ctx)

#include <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

Render keyboard help TUI overlay.

Definition at line 319 of file keyboard_help.c.

319 {
320 if (!ctx) {
321 log_error("keyboard_help_render: ctx is NULL!");
322 return;
323 }
324
325 log_info("keyboard_help_render: STARTING");
326
327 // Get terminal dimensions
328 int term_width = (int)terminal_get_effective_width();
329 int term_height = (int)terminal_get_effective_height();
330 log_info("keyboard_help_render: term_width=%d, term_height=%d", term_width, term_height);
331
332 // Use available terminal width, capped at preferred width
333 int box_width = term_width;
334 if (box_width > 48)
335 box_width = 48; // Cap at preferred width
336 if (box_width < 30)
337 box_width = 30; // Absolute minimum for readability
338
339 // Calculate centering position (true mathematical centering)
340 // Horizontal centering
341 int start_col = (term_width - box_width) / 2;
342 if (start_col < 0) {
343 start_col = 0;
344 }
345
346 // Calculate box height dynamically based on content
347 // With animations separator line added, the standard help screen is 25 rows
348 int box_height = 25;
349
350 // Vertical centering with dynamic box height
351 int start_row = (term_height - box_height) / 2 - 3; // -3 offset for proper vertical centering
352 if (start_row < 0) {
353 start_row = 0;
354 }
355
356 // Build help screen content
357 const size_t BUFFER_SIZE = 8192; // Increased from 4096 to ensure all content fits
358 char *buffer = SAFE_MALLOC(BUFFER_SIZE, char *);
359 size_t buf_pos = 0;
360
361#define APPEND(fmt, ...) \
362 do { \
363 int written = snprintf(buffer + buf_pos, BUFFER_SIZE - buf_pos, fmt, ##__VA_ARGS__); \
364 if (written > 0) { \
365 buf_pos += written; \
366 } \
367 } while (0)
368
369 // Clear screen and position cursor
370 APPEND("\033[2J"); // Clear screen
371 APPEND("\033[H"); // Cursor to home
372
373 // Build help screen with proper spacing using UTF-8 width-aware padding
374 char line_buf[256];
375 char border_buf[256];
376
377 // Generate top border
378 APPEND("\033[%d;%dH", start_row + 1, start_col + 1);
379 border_buf[0] = '\0';
380 int border_pos = 0;
381 int result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "╔");
382 if (result < 0) {
383 log_error("Failed to write top-left corner: snprintf returned %d", result);
384 } else {
385 border_pos += result;
386 }
387 for (int i = 1; i < box_width - 1; i++) {
388 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "═");
389 if (result < 0) {
390 log_error("Failed to write horizontal line: snprintf returned %d", result);
391 break;
392 }
393 border_pos += result;
394 }
395 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "╗");
396 if (result < 0) {
397 log_error("Failed to write top-right corner: snprintf returned %d", result);
398 }
399 APPEND("%s", border_buf);
400
401 // Title
402 APPEND("\033[%d;%dH", start_row + 2, start_col + 1);
403 build_help_line(line_buf, sizeof(line_buf), "ascii-chat Keyboard Shortcuts", box_width);
404 APPEND("%s", line_buf);
405
406 // Generate separator border
407 APPEND("\033[%d;%dH", start_row + 3, start_col + 1);
408 border_buf[0] = '\0';
409 border_pos = 0;
410 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "╠");
411 if (result < 0) {
412 log_error("Failed to write left T: snprintf returned %d", result);
413 } else {
414 border_pos += result;
415 }
416 for (int i = 1; i < box_width - 1; i++) {
417 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "═");
418 if (result < 0) {
419 log_error("Failed to write horizontal line: snprintf returned %d", result);
420 break;
421 }
422 border_pos += result;
423 }
424 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "╣");
425 if (result < 0) {
426 log_error("Failed to write right T: snprintf returned %d", result);
427 }
428 APPEND("%s", border_buf);
429
430 // Navigation section
431 int current_row = 4;
432 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
433 "Navigation & Control:");
434 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
435 "─────────────────────");
436 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
437 "? Toggle this help screen");
438 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
439 "Esc Close help / Quit app");
440
441 // Check if media is provided (only show Space/Seek keys if media is loaded)
442 const char *media_url = GET_OPTION(media_url);
443 const char *media_file = GET_OPTION(media_file);
444 bool has_media = (media_url && strlen(media_url) > 0) || (media_file && strlen(media_file) > 0);
445
446 if (has_media) {
447 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
448 "Space Play/Pause (files only)");
449 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
450 "← / → Seek backward/forward 30s");
451 }
452
453 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
454 "m / M Mute/Unmute audio");
455 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
456 "↑ / ↓ Volume up/down (10%)");
457 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
458 "c / C Cycle color mode");
459 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
460 "f / F Cycle color filter");
461 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
462 "x / X Flip webcam horizontally");
463 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
464 "y / Y Flip webcam vertically");
465 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
466 "r / R Cycle render mode");
467
468#ifndef NDEBUG
469 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
470 "` Print current sync primitive state");
471#endif
472
473 // Blank line before settings section
474 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "");
475
476 // Current settings section
477 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Current Settings:");
478 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "───────────────");
479
480 // Get current option values
481 double current_volume = GET_OPTION(speakers_volume);
482 int current_color_mode = (int)GET_OPTION(color_mode);
483 int current_render_mode = (int)GET_OPTION(render_mode);
484 color_filter_t current_color_filter = GET_OPTION(color_filter);
485 bool flip_x = (bool)GET_OPTION(flip_x);
486 bool flip_y = (bool)GET_OPTION(flip_y);
487 bool current_audio = (bool)GET_OPTION(audio_enabled);
488
489 // Format volume bar as "[======== ] 80%"
490 char volume_bar[32];
491 format_volume_bar(current_volume, volume_bar, sizeof(volume_bar));
492
493 // Get string values
494 const char *color_str = color_mode_to_string(current_color_mode);
495 const char *filter_str = color_filter_to_string(current_color_filter);
496 const char *render_str = render_mode_to_string(current_render_mode);
497
498 // Create status indicators for flip, audio, and matrix rain
499 // Format flip status as "rows=X/O cols=X/O" (rows=flip_y, cols=flip_x)
500 char flip_status[64];
501 snprintf(flip_status, sizeof(flip_status), "rows=%s cols=%s", status_indicator(flip_y), status_indicator(flip_x));
502
503 const char *audio_text = status_indicator(current_audio);
504 bool matrix_rain_enabled = GET_OPTION(matrix_rain);
505 const char *matrix_text = status_indicator(matrix_rain_enabled);
506
507 // Build settings lines with UTF-8 width-aware padding (ordered to match keybinds: m, ↑/↓, c, f, x/y, r)
508 append_settings_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Audio",
509 audio_text, 6);
510 append_settings_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Volume",
511 volume_bar, 6);
512 append_settings_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Color", color_str,
513 6);
514 append_settings_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Filter",
515 filter_str, 6);
516 append_settings_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Render",
517 render_str, 6);
518 append_settings_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Flip",
519 flip_status, 6);
520
521 // Blank line before animations section
522 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "");
523
524 // Animations section
525 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
526 "Animations (number key toggle):");
527 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width,
528 "───────────────────────────────");
529
530 // Format: "(0) Matrix \"Digital Rain\" : X/O"
531 char animation_line[256];
532 snprintf(animation_line, sizeof(animation_line), "(0) Matrix \"Digital Rain\" : %s", matrix_text);
533 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, animation_line);
534
535 // FPS Counter toggle
536 char fps_line[256];
537 snprintf(fps_line, sizeof(fps_line), "(-) FPS Counter : %s", status_indicator(GET_OPTION(fps_counter)));
538 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, fps_line);
539
540 // Blank line before footer
541 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "");
542
543 // Footer
544 append_help_line(buffer, &buf_pos, BUFFER_SIZE, start_row, &current_row, start_col, box_width, "Press ? to close");
545
546 // Bottom border
547 int remaining_buf = BUFFER_SIZE - buf_pos;
548 int written = snprintf(buffer + buf_pos, remaining_buf, "\033[%d;%dH", start_row + current_row, start_col + 1);
549 if (written > 0) {
550 buf_pos += written;
551 }
552 border_buf[0] = '\0';
553 border_pos = 0;
554 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "╚");
555 if (result < 0) {
556 log_error("Failed to write bottom-left corner: snprintf returned %d", result);
557 } else {
558 border_pos += result;
559 }
560 for (int i = 1; i < box_width - 1; i++) {
561 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "═");
562 if (result < 0) {
563 log_error("Failed to write horizontal line: snprintf returned %d", result);
564 break;
565 }
566 border_pos += result;
567 }
568 result = SAFE_SNPRINTF(border_buf + border_pos, sizeof(border_buf) - border_pos, "%s", "╝");
569 if (result < 0) {
570 log_error("Failed to write bottom-right corner: snprintf returned %d", result);
571 }
572 written = snprintf(buffer + buf_pos, BUFFER_SIZE - buf_pos, "%s", border_buf);
573 if (written > 0) {
574 buf_pos += written;
575 }
576
577#undef APPEND
578
579 log_info("keyboard_help_render: buffer prepared, buf_pos=%zu", buf_pos);
580
581 // Write buffer to terminal
582 session_display_write_raw(ctx, buffer, buf_pos);
583 log_info("keyboard_help_render: buffer written to terminal");
584
585 // Flush output
586 if (ctx && session_display_has_tty(ctx)) {
587 int tty_fd = session_display_get_tty_fd(ctx);
588 log_info("keyboard_help_render: tty_fd=%d", tty_fd);
589 if (tty_fd >= 0) {
590 (void)terminal_flush(tty_fd);
591 log_info("keyboard_help_render: terminal flushed");
592 }
593 }
594
595 SAFE_FREE(buffer);
596 log_info("keyboard_help_render: COMPLETE");
597}
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:492
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
asciichat_error_t terminal_flush(int fd)
Flush terminal output.
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.
bool session_display_has_tty(session_display_ctx_t *ctx)
Check if display has a TTY (terminal) available.
int session_display_get_tty_fd(session_display_ctx_t *ctx)
Get the TTY file descriptor.
#define APPEND(fmt,...)
const char * color_mode_to_string(terminal_color_mode_t mode)
Convert color mode enum to string.
const char * render_mode_to_string(render_mode_t mode)
unsigned short int terminal_get_effective_width(void)
Get effective terminal width with fallback priority.
unsigned short int terminal_get_effective_height(void)
Get effective terminal height with fallback priority.
#define bool
Definition stdbool.h:61
color_filter_t
Monochromatic color filter enumeration.
Definition terminal.h:599

References APPEND, bool, color_mode_to_string(), GET_OPTION, log_error, log_info, render_mode_to_string(), SAFE_FREE, SAFE_MALLOC, SAFE_SNPRINTF, session_display_get_tty_fd(), session_display_has_tty(), session_display_write_raw(), terminal_flush(), terminal_get_effective_height(), and terminal_get_effective_width().

Referenced by display_render_frame(), session_handle_keyboard_input(), and session_render_loop().

◆ keyboard_help_signal_cancel()

void keyboard_help_signal_cancel ( void  )

#include <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 1201 of file src/common/session/display.c.

1201 {
1202 atomic_store_bool(&g_help_signal_cancel, true);
1203}
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177

References atomic_store_bool().

◆ keyboard_help_toggle()

void keyboard_help_toggle ( session_display_ctx_t *  ctx)

#include <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
Thread-safe: Uses atomic bool for lock-free toggle

Toggle keyboard help on/off.

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

1160 {
1161 if (!ctx) {
1162 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL");
1163 return;
1164 }
1165
1166 bool current = atomic_load_bool(&ctx->keyboard_help_active);
1167 atomic_store_bool(&ctx->keyboard_help_active, !current);
1168}

References atomic_load_bool(), atomic_store_bool(), ERROR_INVALID_PARAM, session_display_ctx::keyboard_help_active, and SET_ERRNO.

Referenced by keyboard_help_toggle_global(), session_handle_keyboard_input(), and session_render_loop().

◆ keyboard_help_toggle_global()

void keyboard_help_toggle_global ( void  )

#include <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 1240 of file src/common/session/display.c.

1240 {
1241 session_display_ctx_t *ctx = get_current_display_ctx();
1242 if (ctx) {
1244 }
1245}
void keyboard_help_toggle(session_display_ctx_t *ctx)
Toggle keyboard help on/off (implemented in display.c for struct access)

References keyboard_help_toggle().

◆ session_audio_add_source()

asciichat_error_t session_audio_add_source ( session_audio_ctx_t *  ctx,
uint32_t  source_id 
)

#include <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 220 of file src/common/session/audio.c.

220 {
221 if (!ctx || !ctx->initialized) {
222 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_add_source: invalid context");
223 }
224
225 if (!ctx->is_host) {
226 return SET_ERRNO(ERROR_INVALID_STATE, "session_audio_add_source: not a host context");
227 }
228
229 // Check if source already exists
230 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
231 if (ctx->sources[i].active && ctx->sources[i].source_id == source_id) {
232 return ASCIICHAT_OK; // Already registered
233 }
234 }
235
236 // Find empty slot
237 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
238 if (!ctx->sources[i].active) {
239 ctx->sources[i].source_id = source_id;
240 ctx->sources[i].active = true;
242 if (!ctx->sources[i].buffer) {
243 ctx->sources[i].active = false;
244 return SET_ERRNO(ERROR_MEMORY, "Failed to create audio buffer for source");
245 }
246 ctx->source_count++;
247 return ASCIICHAT_OK;
248 }
249 }
250
251 return SET_ERRNO(ERROR_RESOURCE_EXHAUSTED, "Maximum audio sources reached");
252}
audio_ring_buffer_t * audio_ring_buffer_create(void)
Create a new audio ring buffer (for playback with jitter buffering)
@ ERROR_INVALID_STATE
@ ERROR_RESOURCE_EXHAUSTED
@ ERROR_MEMORY
Definition error_codes.h:56
#define SESSION_AUDIO_MAX_SOURCES
Maximum number of audio sources for mixing.
int source_count
Number of active sources (host only)
session_audio_source_t sources[32]
Audio sources for mixing (host only)
bool initialized
Context is fully initialized.
bool is_host
True if this is a host context (has mixing capabilities)

References session_audio_source_t::active, ASCIICHAT_OK, audio_ring_buffer_create(), 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 <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 74 of file src/common/session/audio.c.

74 {
75 // Allocate context
77
78 ctx->is_host = is_host;
79 ctx->running = false;
80
81 // Initialize underlying audio context
83 if (result != ASCIICHAT_OK) {
84 log_error("Failed to initialize audio context: %d", result);
85 SAFE_FREE(ctx);
86 return NULL;
87 }
88
89 // Initialize mixing resources for host
90 if (is_host) {
91 // Allocate mix buffer (enough for one audio buffer worth)
92 ctx->mix_buffer_size = AUDIO_BUFFER_SIZE * 4; // Extra space for safety
93 ctx->mix_buffer = SAFE_CALLOC(ctx->mix_buffer_size, sizeof(float), float *);
94
95 // Initialize source array
96 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
97 ctx->sources[i].source_id = 0;
98 ctx->sources[i].active = false;
99 ctx->sources[i].buffer = NULL;
100 }
101 ctx->source_count = 0;
102 }
103
104 ctx->initialized = true;
105 return ctx;
106}
#define AUDIO_BUFFER_SIZE
Total audio buffer size (frames × channels)
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_capture().

◆ session_audio_destroy()

void session_audio_destroy ( session_audio_ctx_t *  ctx)

#include <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 108 of file src/common/session/audio.c.

108 {
109 if (!ctx) {
110 return;
111 }
112
113 // Stop audio if running
114 if (ctx->running) {
116 }
117
118 // Cleanup host-specific resources
119 if (ctx->is_host) {
120 // Destroy source buffers
121 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
122 if (ctx->sources[i].buffer) {
124 ctx->sources[i].buffer = NULL;
125 }
126 }
127
128 // Free mix buffer
129 if (ctx->mix_buffer) {
130 SAFE_FREE(ctx->mix_buffer);
131 }
132 }
133
134 // Destroy underlying audio context
136
137 ctx->initialized = false;
138 SAFE_FREE(ctx);
139}
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.

References session_audio_ctx::audio_ctx, audio_destroy(), audio_ring_buffer_destroy(), session_audio_source_t::buffer, session_audio_ctx::initialized, session_audio_ctx::is_host, session_audio_ctx::mix_buffer, 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_start_render(), session_host_stop_render(), and session_participant_destroy().

◆ session_audio_is_running()

bool session_audio_is_running ( session_audio_ctx_t *  ctx)

#include <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 189 of file src/common/session/audio.c.

189 {
190 if (!ctx || !ctx->initialized) {
191 return false;
192 }
193 return ctx->running;
194}

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 <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 296 of file src/common/session/audio.c.

296 {
297 if (!ctx || !ctx->initialized || !output || num_samples == 0) {
298 return 0;
299 }
300
301 if (!ctx->is_host) {
302 return 0;
303 }
304
305 // Initialize output to silence
306 memset(output, 0, num_samples * sizeof(float));
307
308 // Ensure we have a mix buffer
309 if (!ctx->mix_buffer || num_samples > ctx->mix_buffer_size) {
310 return 0;
311 }
312
313 int sources_mixed = 0;
314
315 // Mix all active sources except the excluded one
316 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
317 if (!ctx->sources[i].active || ctx->sources[i].source_id == exclude_id) {
318 continue;
319 }
320
321 if (!ctx->sources[i].buffer) {
322 continue;
323 }
324
325 // Read samples from this source
326 size_t samples_read = audio_ring_buffer_read(ctx->sources[i].buffer, ctx->mix_buffer, num_samples);
327
328 if (samples_read == 0) {
329 continue;
330 }
331
332 // Add to output mix
333 for (size_t j = 0; j < samples_read; j++) {
334 output[j] += ctx->mix_buffer[j];
335 }
336
337 sources_mixed++;
338 }
339
340 // Apply simple clipping prevention if multiple sources were mixed
341 if (sources_mixed > 1) {
342 float scale = 1.0f / (float)sources_mixed;
343 for (size_t i = 0; i < num_samples; i++) {
344 output[i] *= scale;
345 // Soft clipping
346 if (output[i] > 1.0f) {
347 output[i] = 1.0f;
348 } else if (output[i] < -1.0f) {
349 output[i] = -1.0f;
350 }
351 }
352 }
353
354 return num_samples;
355}
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 <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 200 of file src/common/session/audio.c.

200 {
201 if (!ctx || !ctx->initialized || !buffer || num_samples == 0) {
202 return 0;
203 }
204
205 return audio_ring_buffer_read(ctx->audio_ctx.capture_buffer, buffer, num_samples);
206}
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 <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 254 of file src/common/session/audio.c.

254 {
255 if (!ctx || !ctx->initialized || !ctx->is_host) {
256 return;
257 }
258
259 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
260 if (ctx->sources[i].active && ctx->sources[i].source_id == source_id) {
261 if (ctx->sources[i].buffer) {
263 ctx->sources[i].buffer = NULL;
264 }
265 ctx->sources[i].active = false;
266 ctx->sources[i].source_id = 0;
267 ctx->source_count--;
268 return;
269 }
270 }
271}

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 <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 145 of file src/common/session/audio.c.

145 {
146 if (!ctx || !ctx->initialized) {
147 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_start_capture: invalid context");
148 }
149
150 // Use full duplex for proper echo cancellation
151 return session_audio_start_duplex(ctx);
152}
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 <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 163 of file src/common/session/audio.c.

163 {
164 if (!ctx || !ctx->initialized) {
165 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_start_duplex: invalid context");
166 }
167
168 if (ctx->running) {
169 return ASCIICHAT_OK; // Already running
170 }
171
173 if (result == ASCIICHAT_OK) {
174 ctx->running = true;
175 }
176
177 return result;
178}
asciichat_error_t audio_start_duplex(audio_context_t *ctx)
Start full-duplex audio (simultaneous capture and playback)

References ASCIICHAT_OK, session_audio_ctx::audio_ctx, audio_start_duplex(), ERROR_INVALID_PARAM, session_audio_ctx::initialized, session_audio_ctx::running, and SET_ERRNO.

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

◆ session_audio_start_playback()

asciichat_error_t session_audio_start_playback ( session_audio_ctx_t *  ctx)

#include <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 154 of file src/common/session/audio.c.

154 {
155 if (!ctx || !ctx->initialized) {
156 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_start_playback: invalid context");
157 }
158
159 // Use full duplex for proper echo cancellation
160 return session_audio_start_duplex(ctx);
161}

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 <audio.h>

Stop all audio streams.

Parameters
ctxAudio context (must not be NULL)

Stops capture, playback, and any mixing operations.

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

180 {
181 if (!ctx || !ctx->initialized || !ctx->running) {
182 return;
183 }
184
185 (void)audio_stop_duplex(&ctx->audio_ctx);
186 ctx->running = false;
187}
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_participant_start_audio_capture(), and session_participant_stop_audio_capture().

◆ session_audio_write_playback()

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

#include <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 208 of file src/common/session/audio.c.

208 {
209 if (!ctx || !ctx->initialized || !buffer) {
210 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_write_playback: invalid parameter");
211 }
212
213 return audio_write_samples(&ctx->audio_ctx, buffer, (int)num_samples);
214}
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 <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 273 of file src/common/session/audio.c.

274 {
275 if (!ctx || !ctx->initialized || !samples) {
276 return SET_ERRNO(ERROR_INVALID_PARAM, "session_audio_write_source: invalid parameter");
277 }
278
279 if (!ctx->is_host) {
280 return SET_ERRNO(ERROR_INVALID_STATE, "session_audio_write_source: not a host context");
281 }
282
283 // Find the source
284 for (int i = 0; i < SESSION_AUDIO_MAX_SOURCES; i++) {
285 if (ctx->sources[i].active && ctx->sources[i].source_id == source_id) {
286 if (ctx->sources[i].buffer) {
287 return audio_ring_buffer_write(ctx->sources[i].buffer, samples, (int)num_samples);
288 }
289 return SET_ERRNO(ERROR_INVALID_STATE, "Source buffer not initialized");
290 }
291 }
292
293 return SET_ERRNO(ERROR_NOT_FOUND, "Audio source not found: %u", source_id);
294}
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 <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 483 of file common/session/capture.c.

483 {
484 if (!ctx || !ctx->initialized || !ctx->source) {
485 return true;
486 }
487
488 return media_source_at_end(ctx->source);
489}
bool media_source_at_end(media_source_t *source)
Check if media source reached end of stream.
Definition source.c:772
bool initialized
Context has been successfully initialized.
media_source_t * source
Underlying media source (webcam, file, stdin, test)

References session_capture_ctx::initialized, media_source_at_end(), and session_capture_ctx::source.

◆ session_capture_create()

session_capture_ctx_t * session_capture_create ( const session_capture_config_t *  config)

#include <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 193 of file common/session/capture.c.

193 {
194 // Auto-create config from command-line options if NULL
195 session_capture_config_t auto_config = {0};
196 if (!config) {
197 // Auto-initialization from GET_OPTION() only works when options have been parsed
198 // Verify by checking if we can access options (would SET_ERRNO if not)
199 const char *media_file = GET_OPTION(media_file);
200 bool media_from_stdin = GET_OPTION(media_from_stdin);
201
202 if (media_file[0] != '\0') {
203 // File or stdin streaming
204 auto_config.type = media_from_stdin ? MEDIA_SOURCE_STDIN : MEDIA_SOURCE_FILE;
205 auto_config.path = media_file;
206 auto_config.loop = GET_OPTION(media_loop) && !media_from_stdin;
207 } else if (GET_OPTION(test_pattern)) {
208 // Test pattern mode
209 auto_config.type = MEDIA_SOURCE_TEST;
210 auto_config.path = NULL;
211 } else {
212 // Webcam mode (default)
213 static char webcam_index_str[32];
214 unsigned int idx = GET_OPTION(webcam_index);
215 safe_snprintf(webcam_index_str, sizeof(webcam_index_str), "%u", idx);
216 auto_config.type = MEDIA_SOURCE_WEBCAM;
217 auto_config.path = webcam_index_str;
218 log_debug("Webcam mode: using device index %u", idx);
219 }
220
221 // Default settings suitable for local display
222 auto_config.target_fps = 60;
223 auto_config.resize_for_network = false;
224 config = &auto_config;
225 }
226
227 // Check if we should exit before starting initialization
228 if (config->should_exit_callback && config->should_exit_callback(config->callback_data)) {
229 return NULL;
230 }
231
232 // Allocate context
234
235 // Store configuration
236 ctx->target_fps = config->target_fps > 0 ? config->target_fps : 60;
238
239 // Store audio configuration
240 ctx->audio_enabled = config->enable_audio;
242 ctx->mic_audio_ctx = config->mic_audio_ctx;
243 ctx->using_file_audio = false;
244 ctx->file_has_audio = false;
245
246 // Create media source from config type and path
247 ctx->source = media_source_create(config->type, config->path);
248
249 if (!ctx->source) {
250 // Preserve existing error if set, otherwise set generic error
251 asciichat_error_t existing_error = GET_ERRNO();
252 if (existing_error == ASCIICHAT_OK) {
253 SET_ERRNO(ERROR_MEDIA_INIT, "Failed to create media source");
254 }
255 SAFE_FREE(ctx);
256 return NULL;
257 }
258
261 SAFE_FREE(ctx);
262 return NULL;
263 }
264
265 // Set up exit callback on media source for graceful shutdown during I/O
266 // This allows Ctrl+C to abort blocking FFmpeg calls like YouTube HTTP requests
267 if (config->should_exit_callback && ctx->source) {
269 }
270
271 // Detect if media source has audio
272 if (ctx->audio_enabled && ctx->source) {
274 if (ctx->file_has_audio) {
275 ctx->using_file_audio = true;
276 log_info("Audio capture enabled: using file audio");
277 } else if (ctx->audio_fallback_enabled && ctx->mic_audio_ctx) {
278 ctx->using_file_audio = false;
279 log_info("Audio capture enabled: file has no audio, using microphone fallback");
280 } else {
281 ctx->audio_enabled = false;
282 log_debug("Audio capture disabled: no file audio and no fallback configured");
283 }
284 }
285
286 // Enable loop if requested (only for file sources)
287 if (config->loop && config->type == MEDIA_SOURCE_FILE) {
288 media_source_set_loop(ctx->source, true);
289 }
290
291 // Set initial pause state if requested (only for file sources)
292 // Note: We don't pause yet - we'll pause AFTER reading the first frame in the render loop
293 // This is because paused sources return NULL from read_video()
294 if (config->type == MEDIA_SOURCE_FILE && GET_OPTION(pause)) {
296 log_debug("Will pause after first frame (--pause flag)");
297 }
298
299 // Perform initial seek if requested
300 if (config->initial_seek_timestamp > 0.0) {
301 log_debug("Seeking to %.2f seconds", config->initial_seek_timestamp);
303 if (seek_err != ASCIICHAT_OK) {
304 log_warn("Failed to seek to %.2f seconds: %d", config->initial_seek_timestamp, seek_err);
305 // Continue anyway - seeking is best-effort
306 } else {
307 log_debug("Successfully seeked to %.2f seconds", config->initial_seek_timestamp);
308 // Codec buffers are flushed during seek, so next frame read will use correct position
309
310 // Reset timing state after seek to prevent FPS calculation confusion
311 // (frame_count and elapsed_time must match the new playback position)
312 ctx->frame_count = 0;
313 ctx->start_time_ns = time_get_ns();
314 }
315 }
316
317 // Initialize adaptive sleep for frame rate limiting
318 // Calculate baseline sleep time in nanoseconds from target FPS
319 uint64_t baseline_sleep_ns = NS_PER_SEC_INT / ctx->target_fps;
320 adaptive_sleep_config_t sleep_config = {
321 .baseline_sleep_ns = baseline_sleep_ns,
322 .min_speed_multiplier = 0.5, // Allow slowing down to 50% of baseline
323 .max_speed_multiplier = 2.0, // Allow speeding up to 200% of baseline
324 .speedup_rate = 0.1, // Adapt by 10% per frame if possible
325 .slowdown_rate = 0.1 // Adapt by 10% per frame if possible
326 };
327 adaptive_sleep_init(&ctx->sleep_state, &sleep_config);
328
329 // Initialize FPS tracker
330 // Note: Must allocate tracker_name on heap since fps_init stores a pointer to it
331 char *tracker_name = SAFE_MALLOC(32, char *);
332 if (!tracker_name) {
334 SAFE_FREE(ctx);
335 return NULL;
336 }
337 safe_snprintf(tracker_name, 32, "CAPTURE_%u", ctx->target_fps);
338 fps_init(&ctx->fps_tracker, (int)ctx->target_fps, tracker_name);
339
340 // Record start time for FPS calculation (nanoseconds)
341 ctx->start_time_ns = time_get_ns();
342
343 ctx->initialized = true;
344 return ctx;
345}
void fps_init(fps_t *tracker, int expected_fps, const char *name)
Initialize FPS tracker.
Definition fps.c:32
#define GET_ERRNO()
Get current error code (0 if no error)
@ ERROR_MEDIA_INIT
Definition error_codes.h:70
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
bool media_source_has_audio(media_source_t *source)
Check if media source has audio stream.
Definition source.c:737
void media_source_destroy(media_source_t *source)
Destroy media source and free resources.
Definition source.c:448
asciichat_error_t media_source_seek(media_source_t *source, double timestamp_sec)
Seek media source to timestamp.
Definition source.c:851
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:1047
media_source_t * media_source_create(media_source_type_t type, const char *path)
Create a new media source.
Definition source.c:187
void media_source_set_loop(media_source_t *source, bool loop)
Enable or disable looping.
Definition source.c:760
@ MEDIA_SOURCE_STDIN
Piped or redirected input.
Definition source.h:84
@ MEDIA_SOURCE_FILE
Media file (video/audio)
Definition source.h:83
@ MEDIA_SOURCE_TEST
Test pattern generator.
Definition source.h:85
#define NS_PER_SEC_INT
Definition time.h:157
void adaptive_sleep_init(adaptive_sleep_state_t *state, const adaptive_sleep_config_t *config)
Initialize adaptive sleep state with configuration.
Definition util/time.c:609
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
asciichat_error_t media_source_start_video(media_source_t *source)
Definition source.c:506
Configuration for adaptive sleep behavior.
Definition time.h:677
uint64_t baseline_sleep_ns
Normal sleep time in nanoseconds (when queue is at target)
Definition time.h:678
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: callback to check if initialization should be cancelled (e.g., shutdown signal)
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.
void * mic_audio_ctx
Microphone audio context for fallback (borrowed, not owned)
bool file_has_audio
File has audio stream available.
adaptive_sleep_state_t sleep_state
Adaptive sleep state for frame rate limiting.
bool using_file_audio
Using file audio (true) or microphone fallback (false)
bool 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.

References adaptive_sleep_init(), ASCIICHAT_OK, session_capture_ctx::audio_enabled, session_capture_ctx::audio_fallback_enabled, session_capture_config_t::audio_fallback_to_mic, adaptive_sleep_config_t::baseline_sleep_ns, session_capture_config_t::callback_data, session_capture_config_t::enable_audio, ERROR_MEDIA_INIT, session_capture_ctx::file_has_audio, fps_init(), session_capture_ctx::fps_tracker, session_capture_ctx::frame_count, GET_ERRNO, GET_OPTION, session_capture_config_t::initial_seek_timestamp, session_capture_ctx::initialized, log_debug, log_info, log_warn, session_capture_config_t::loop, media_source_create(), media_source_destroy(), MEDIA_SOURCE_FILE, 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_ctx::mic_audio_ctx, session_capture_config_t::mic_audio_ctx, NS_PER_SEC_INT, session_capture_config_t::path, session_capture_ctx::resize_for_network, session_capture_config_t::resize_for_network, SAFE_CALLOC, SAFE_FREE, SAFE_MALLOC, safe_snprintf(), SET_ERRNO, session_capture_config_t::should_exit_callback, session_capture_ctx::should_pause_after_first_frame, session_capture_ctx::sleep_state, session_capture_ctx::source, session_capture_ctx::start_time_ns, session_capture_ctx::target_fps, session_capture_config_t::target_fps, time_get_ns(), 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_capture().

◆ session_capture_destroy()

void session_capture_destroy ( session_capture_ctx_t *  ctx)

#include <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 347 of file common/session/capture.c.

347 {
348 if (!ctx) {
349 return;
350 }
351
352 // Destroy media source
353 if (ctx->source) {
355 ctx->source = NULL;
356 }
357
358 // Free FPS tracker name (allocated in session_capture_create)
359 if (ctx->fps_tracker.tracker_name) {
360 char *temp = (char *)ctx->fps_tracker.tracker_name;
361 SAFE_FREE(temp);
362 }
363
364 ctx->initialized = false;
365 SAFE_FREE(ctx);
366}
const char * tracker_name
Definition fps.h:58

References session_capture_ctx::fps_tracker, session_capture_ctx::initialized, media_source_destroy(), SAFE_FREE, session_capture_ctx::source, and fps_t::tracker_name.

Referenced by capture_cleanup(), display_cleanup(), session_client_like_run(), and session_participant_destroy().

◆ session_capture_get_current_fps()

double session_capture_get_current_fps ( session_capture_ctx_t *  ctx)

#include <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 495 of file common/session/capture.c.

495 {
496 if (!ctx || !ctx->initialized || ctx->frame_count == 0) {
497 return 0.0;
498 }
499
500 // Calculate elapsed time in nanoseconds then convert to seconds
502 double elapsed_sec = time_ns_to_s(elapsed_ns);
503
504 if (elapsed_sec <= 0.0) {
505 return 0.0;
506 }
507
508 return (double)ctx->frame_count / elapsed_sec;
509}
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition util/time.c:150

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

◆ session_capture_get_media_source()

void * session_capture_get_media_source ( session_capture_ctx_t *  ctx)

#include <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 559 of file common/session/capture.c.

559 {
560 if (!ctx || !ctx->initialized || !ctx->source) {
561 return NULL;
562 }
563 return (void *)ctx->source;
564}

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

Referenced by capture_get_media_source(), session_client_like_run(), session_handle_keyboard_input(), session_participant_start_audio_capture(), and session_pipeline_create().

◆ session_capture_get_target_fps()

uint32_t session_capture_get_target_fps ( session_capture_ctx_t *  ctx)

#include <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 511 of file common/session/capture.c.

511 {
512 if (!ctx) {
513 return 0;
514 }
515 return ctx->target_fps;
516}

References session_capture_ctx::target_fps.

Referenced by session_capture_sleep_for_fps().

◆ session_capture_has_audio()

bool session_capture_has_audio ( session_capture_ctx_t *  ctx)

#include <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 518 of file common/session/capture.c.

518 {
519 if (!ctx || !ctx->initialized) {
520 return false;
521 }
522 return ctx->audio_enabled;
523}

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 <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 491 of file common/session/capture.c.

491 {
492 return ctx != NULL && ctx->initialized && ctx->source != NULL;
493}

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 <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 414 of file common/session/capture.c.

414 {
415 if (!ctx || !frame) {
416 SET_ERRNO(ERROR_INVALID_PARAM, "session_capture_process_for_transmission: NULL parameter");
417 return NULL;
418 }
419
420 // If resize is not enabled, just create a copy
421 if (!ctx->resize_for_network) {
422 image_t *copy = image_new(frame->w, frame->h);
423 if (!copy) {
424 SET_ERRNO(ERROR_MEMORY, "Failed to allocate image copy");
425 return NULL;
426 }
427 memcpy(copy->pixels, frame->pixels, (size_t)frame->w * (size_t)frame->h * sizeof(rgb_pixel_t));
428 return copy;
429 }
430
431 // Calculate optimal dimensions for network transmission
432 ssize_t resized_width, resized_height;
433 calculate_optimal_dimensions(frame->w, frame->h, SESSION_MAX_FRAME_WIDTH, SESSION_MAX_FRAME_HEIGHT, &resized_width,
434 &resized_height);
435
436 // Check if resizing is actually needed
437 if (frame->w == resized_width && frame->h == resized_height) {
438 // No resizing needed - create a copy
439 image_t *copy = image_new(frame->w, frame->h);
440 if (!copy) {
441 SET_ERRNO(ERROR_MEMORY, "Failed to allocate image copy");
442 return NULL;
443 }
444 memcpy(copy->pixels, frame->pixels, (size_t)frame->w * (size_t)frame->h * sizeof(rgb_pixel_t));
445 return copy;
446 }
447
448 // Create new image for resized frame
449 image_t *resized = image_new(resized_width, resized_height);
450 if (!resized) {
451 SET_ERRNO(ERROR_MEMORY, "Failed to allocate resized image buffer");
452 return NULL;
453 }
454
455 // Perform resizing operation
456 image_resize(frame, resized);
457
458 return resized;
459}
#define SESSION_MAX_FRAME_WIDTH
Maximum frame width for network transmission (bandwidth optimization)
#define SESSION_MAX_FRAME_HEIGHT
Maximum frame height for network transmission (bandwidth optimization)
void image_resize(const image_t *s, image_t *d)
Resize image using nearest-neighbor interpolation.
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order)
RGB pixel structure.

References ERROR_INVALID_PARAM, ERROR_MEMORY, frame, image_new(), image_resize(), image_t::pixels, session_capture_ctx::resize_for_network, SESSION_MAX_FRAME_HEIGHT, SESSION_MAX_FRAME_WIDTH, and SET_ERRNO.

◆ session_capture_read_audio()

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

#include <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 525 of file common/session/capture.c.

525 {
526 if (!ctx || !ctx->initialized || !buffer || num_samples == 0) {
527 return 0;
528 }
529
530 if (!ctx->audio_enabled) {
531 return 0;
532 }
533
534 // If using file audio, read from media source
535 if (ctx->using_file_audio && ctx->source) {
536 return media_source_read_audio(ctx->source, buffer, num_samples);
537 }
538
539 // If using microphone fallback, read from mic context
540 if (ctx->mic_audio_ctx) {
541 // mic_audio_ctx is actually an audio_context_t pointer
542 // We need to read from its capture_buffer audio ring buffer
543 audio_context_t *audio_ctx = (audio_context_t *)ctx->mic_audio_ctx;
544 if (audio_ctx && audio_ctx->capture_buffer) {
545 return audio_ring_buffer_read(audio_ctx->capture_buffer, buffer, num_samples);
546 }
547 }
548
549 return 0;
550}
size_t media_source_read_audio(media_source_t *source, float *buffer, size_t num_samples)
Read audio samples from media source.
Definition source.c:652
Audio context for full-duplex capture and playback.

References session_capture_ctx::audio_enabled, audio_ring_buffer_read(), audio_context_t::capture_buffer, session_capture_ctx::initialized, media_source_read_audio(), session_capture_ctx::mic_audio_ctx, session_capture_ctx::source, and session_capture_ctx::using_file_audio.

◆ session_capture_read_frame()

image_t * session_capture_read_frame ( session_capture_ctx_t *  ctx)

#include <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 372 of file common/session/capture.c.

372 {
373 if (!ctx || !ctx->initialized || !ctx->source) {
374 return NULL;
375 }
376
377 static uint64_t last_frame_time_ns = 0;
378 uint64_t frame_request_time_ns = time_get_ns();
379
381
382 if (frame) {
383 uint64_t frame_available_time_ns = time_get_ns();
384
385 // Track frame for FPS reporting
386 fps_frame_ns(&ctx->fps_tracker, frame_available_time_ns, "frame captured");
387 ctx->frame_count++;
388
389 // Log frame-to-frame timing
390 if (last_frame_time_ns > 0) {
391 uint64_t time_since_last_frame_ns = time_elapsed_ns(last_frame_time_ns, frame_request_time_ns);
392 uint64_t time_to_get_frame_ns = time_elapsed_ns(frame_request_time_ns, frame_available_time_ns);
393 double since_last_ms = (double)time_since_last_frame_ns / NS_PER_MS;
394 double to_get_ms = (double)time_to_get_frame_ns / NS_PER_MS;
395
396 if (ctx->frame_count % 30 == 0) {
397 log_dev_every(3 * US_PER_SEC_INT, "FRAME_TIMING[%lu]: since_last=%.1f ms, to_get=%.1f ms", ctx->frame_count,
398 since_last_ms, to_get_ms);
399 }
400 }
401 last_frame_time_ns = frame_available_time_ns;
402
403 // Handle --pause flag: pause after first frame is read
406 ctx->paused_after_first_frame = true;
407 log_info("Paused (--pause flag)");
408 }
409 }
410
411 return frame;
412}
void fps_frame_ns(fps_t *tracker, uint64_t current_time_ns, const char *context)
Track a frame and detect lag conditions (nanosecond version - PRIMARY)
Definition fps.c:52
void media_source_pause(media_source_t *source)
Pause media playback.
Definition source.c:1004
image_t * media_source_read_video(media_source_t *source)
Read next video frame from media source.
Definition source.c:523
#define NS_PER_MS
Definition time.h:147
#define US_PER_SEC_INT
Definition time.h:161
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:699
bool paused_after_first_frame
Whether we've already paused after the first frame.

References fps_frame_ns(), session_capture_ctx::fps_tracker, frame, session_capture_ctx::frame_count, session_capture_ctx::initialized, log_dev_every, log_info, media_source_pause(), media_source_read_video(), NS_PER_MS, session_capture_ctx::paused_after_first_frame, session_capture_ctx::should_pause_after_first_frame, session_capture_ctx::source, time_elapsed_ns(), time_get_ns(), and US_PER_SEC_INT.

◆ session_capture_sleep_for_fps()

void session_capture_sleep_for_fps ( session_capture_ctx_t *  ctx)

#include <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 461 of file common/session/capture.c.

461 {
462 if (!ctx || !ctx->initialized || !ctx->source) {
463 return;
464 }
465
466 // For file/stdin sources: use direct FPS sleep (no adaptation)
467 // This avoids the adaptive sleep multipliers which cause 2x slowdown
469 if (source_type == MEDIA_SOURCE_FILE || source_type == MEDIA_SOURCE_STDIN) {
471 if (target_fps > 0) {
472 uint64_t sleep_ns = NS_PER_SEC_INT / target_fps;
473 platform_sleep_ns(sleep_ns);
474 }
475 return;
476 }
477
478 // For network/webcam sources: use adaptive sleep to handle variable network conditions
479 // queue_depth=0, target_depth=0 maintains constant frame rate
480 adaptive_sleep_do(&ctx->sleep_state, 0, 0);
481}
media_source_type_t
Media source type enumeration.
Definition source.h:81
media_source_type_t media_source_get_type(media_source_t *source)
Get media source type.
Definition source.c:930
void adaptive_sleep_do(adaptive_sleep_state_t *state, size_t queue_depth, size_t target_depth)
Calculate sleep time and immediately sleep for that duration.
Definition util/time.c:693
void platform_sleep_ns(uint64_t ns)
Platform-safe sleep function with nanosecond precision.
uint32_t session_capture_get_target_fps(session_capture_ctx_t *ctx)
Get the target FPS configured for this capture context.

References adaptive_sleep_do(), session_capture_ctx::initialized, MEDIA_SOURCE_FILE, media_source_get_type(), MEDIA_SOURCE_STDIN, NS_PER_SEC_INT, platform_sleep_ns(), session_capture_get_target_fps(), session_capture_ctx::sleep_state, and session_capture_ctx::source.

◆ session_capture_using_file_audio()

bool session_capture_using_file_audio ( session_capture_ctx_t *  ctx)

#include <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 552 of file common/session/capture.c.

552 {
553 if (!ctx || !ctx->initialized) {
554 return false;
555 }
556 return ctx->using_file_audio;
557}

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 <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 93 of file client_like.c.

93 {
94 return g_stdin_reader;
95}

◆ session_display_clear()

void session_display_clear ( session_display_ctx_t *  ctx)

#include <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 1064 of file src/common/session/display.c.

1064 {
1065 if (!ctx || !ctx->initialized) {
1066 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL or uninitialized");
1067 return;
1068 }
1069
1070 // Skip terminal clear in snapshot mode to preserve rendered output
1071 if (ctx->snapshot_mode) {
1072 return;
1073 }
1074
1075 // Only perform terminal operations when we have a valid TTY (not when piping)
1076 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
1077 (void)terminal_clear_screen();
1078 (void)terminal_cursor_home(ctx->tty_info.fd);
1079 }
1080}
asciichat_error_t terminal_cursor_home(int fd)
Move cursor to home position (top-left)
asciichat_error_t terminal_clear_screen(void)
Clear the terminal screen.
bool has_tty
True if we have a valid TTY for interactive output.
tty_info_t tty_info
TTY information (file descriptor, path, ownership)
bool initialized
Context is fully initialized.
bool snapshot_mode
Snapshot mode enabled.
int fd
File descriptor for TTY access.
Definition terminal.h:755

References ERROR_INVALID_PARAM, tty_info_t::fd, session_display_ctx::has_tty, session_display_ctx::initialized, SET_ERRNO, session_display_ctx::snapshot_mode, terminal_clear_screen(), terminal_cursor_home(), and session_display_ctx::tty_info.

◆ session_display_convert_to_ascii()

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

#include <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 473 of file src/common/session/display.c.

473 {
474 if (!ctx) {
475 SET_ERRNO(ERROR_INVALID_PARAM, "session_display_convert_to_ascii: ctx is NULL");
476 return NULL;
477 }
478
479 if (!ctx->initialized) {
480 SET_ERRNO(ERROR_INVALID_STATE, "session_display_convert_to_ascii: ctx not initialized");
481 return NULL;
482 }
483
484 if (!image) {
485 SET_ERRNO(ERROR_INVALID_PARAM, "session_display_convert_to_ascii: image is NULL");
486 return NULL;
487 }
488
489 // Get conversion parameters from command-line options
490 unsigned short int width = GET_OPTION(width);
491 unsigned short int height = GET_OPTION(height);
492 bool stretch = GET_OPTION(stretch);
493 bool preserve_aspect_ratio = !stretch;
494
495 // Determine if we should apply flip_x and flip_y
496 bool flip_x_enabled = GET_OPTION(flip_x);
497 bool flip_y_enabled = GET_OPTION(flip_y);
498
499 color_filter_t color_filter = GET_OPTION(color_filter);
500
501 // Handle dynamic matrix rain effect toggle
502 bool matrix_rain_enabled = GET_OPTION(matrix_rain);
503 if (matrix_rain_enabled && !ctx->digital_rain) {
504 // Initialize digital rain if it's now enabled but wasn't before
505 unsigned short int width_us = terminal_get_effective_width();
506 unsigned short int height_us = terminal_get_effective_height();
507 int width_for_rain = (int)width_us;
508 int height_for_rain = (int)height_us;
509 ctx->digital_rain = digital_rain_init(width_for_rain, height_for_rain);
510 if (ctx->digital_rain) {
512 log_info("Matrix rain effect: enabled");
513 }
514 } else if (!matrix_rain_enabled && ctx->digital_rain) {
515 // Disable digital rain if it's now disabled but was enabled before
517 ctx->digital_rain = NULL;
518 log_info("Matrix rain effect: disabled");
519 }
520
521 // Make a mutable copy of terminal capabilities for ascii_convert_with_capabilities
522 terminal_capabilities_t caps_copy = ctx->caps;
523
524 // Re-evaluate render_mode and color_level on every frame to pick up live changes
525 caps_copy.render_mode = (render_mode_t)GET_OPTION(render_mode);
526 terminal_color_mode_t color_mode_opt = (terminal_color_mode_t)GET_OPTION(color_mode);
527 if (color_mode_opt != TERM_COLOR_AUTO) {
528 caps_copy.color_level = color_mode_opt;
529 }
530
531 // MEASURE EVERY OPERATION - Debug systematic timing
532 uint64_t t_flip_start = time_get_ns();
533
534 // Apply horizontal and/or vertical flips if requested
535 image_t *flipped_image = NULL;
536 const image_t *display_image = image;
537
538 if ((flip_x_enabled || flip_y_enabled) && image->w > 1 && image->h > 1 && image->pixels) {
539 START_TIMER("image_flip");
540 uint64_t t_flip_alloc_start = time_get_ns();
541 flipped_image = image_new((size_t)image->w, (size_t)image->h);
542 uint64_t t_flip_alloc_end = time_get_ns();
543
544 if (flipped_image) {
545 uint64_t t_flip_memcpy_start = time_get_ns();
546 // OPTIMIZATION: Copy entire image first (sequential memory access - cache-friendly)
547 memcpy(flipped_image->pixels, image->pixels, (size_t)image->w * (size_t)image->h * sizeof(rgb_pixel_t));
548 uint64_t t_flip_memcpy_end = time_get_ns();
549
550 uint64_t t_flip_reverse_start = time_get_ns();
551
552 // Apply horizontal flip (X-axis)
553 if (flip_x_enabled) {
554#if SIMD_SUPPORT_NEON
555 // Use NEON-accelerated flip on ARM processors
556 image_flip_horizontal_neon(flipped_image);
557#else
558 // Scalar fallback for non-NEON systems
559 for (int y = 0; y < image->h; y++) {
560 rgb_pixel_t *row = &flipped_image->pixels[y * image->w];
561 for (int x = 0; x < image->w / 2; x++) {
562 rgb_pixel_t temp = row[x];
563 row[x] = row[image->w - 1 - x];
564 row[image->w - 1 - x] = temp;
565 }
566 }
567#endif
568 }
569
570 // Apply vertical flip (Y-axis)
571 if (flip_y_enabled) {
572 for (int y = 0; y < image->h / 2; y++) {
573 rgb_pixel_t *top_row = &flipped_image->pixels[y * image->w];
574 rgb_pixel_t *bottom_row = &flipped_image->pixels[(image->h - 1 - y) * image->w];
575 for (int x = 0; x < image->w; x++) {
576 rgb_pixel_t temp = top_row[x];
577 top_row[x] = bottom_row[x];
578 bottom_row[x] = temp;
579 }
580 }
581 }
582
583 uint64_t t_flip_reverse_end = time_get_ns();
584 display_image = flipped_image;
585
586 log_dev("TIMING_FLIP: alloc=%llu us, memcpy=%llu us, flip=%llu us (x=%d, y=%d)",
587 (t_flip_alloc_end - t_flip_alloc_start) / 1000, (t_flip_memcpy_end - t_flip_memcpy_start) / 1000,
588 (t_flip_reverse_end - t_flip_reverse_start) / 1000, flip_x_enabled, flip_y_enabled);
589 }
590 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 3 * NS_PER_MS_INT, "image_flip",
591 "IMAGE_FLIP: Flip complete (%.2f ms)");
592 }
593 uint64_t t_flip_end = time_get_ns();
594
595 uint64_t t_filter_start = time_get_ns();
596
597 // Apply color filter to RGB pixels before ASCII conversion
598 // Rainbow filter is handled separately via ANSI color replacement
599 image_t *filtered_image = NULL;
600 if (color_filter != COLOR_FILTER_NONE && color_filter != COLOR_FILTER_RAINBOW) {
601 // Create a copy to avoid modifying the original
602 filtered_image = image_new((size_t)display_image->w, (size_t)display_image->h);
603 if (filtered_image && filtered_image->pixels && display_image->pixels) {
604 memcpy(filtered_image->pixels, display_image->pixels,
605 (size_t)display_image->w * (size_t)display_image->h * sizeof(rgb_pixel_t));
606
607 // Apply filter to the copy
608 int stride = (int)display_image->w * 3;
609 float time_seconds = (float)t_filter_start / (float)NS_PER_SEC_INT;
610 apply_color_filter((uint8_t *)filtered_image->pixels, (uint32_t)display_image->w, (uint32_t)display_image->h,
611 (uint32_t)stride, color_filter, time_seconds);
612 }
613 }
614
615 const image_t *ascii_input_image = filtered_image ? filtered_image : display_image;
616 uint64_t t_filter_end = time_get_ns();
617
618 uint64_t t_convert_start = time_get_ns();
619 // Call the standard ASCII conversion using the filtered (or original) image
620 START_TIMER("ascii_convert_with_capabilities");
621 char *result = ascii_convert_with_capabilities(ascii_input_image, width, height, &caps_copy, preserve_aspect_ratio,
622 stretch, ctx->palette_chars);
623 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 5 * NS_PER_MS_INT, "ascii_convert_with_capabilities",
624 "ASCII_CONVERT: Conversion complete (%.2f ms)");
625 uint64_t t_convert_end = time_get_ns();
626
627 // Apply rainbow color filter to ANSI output by replacing RGB values
628 // This preserves character selection while applying the filter colors
629 if (result && color_filter == COLOR_FILTER_RAINBOW) {
630 uint64_t t_color_replace_start = time_get_ns();
631 float time_seconds = (float)t_filter_start / (float)NS_PER_SEC_INT;
632
633 char *rainbow_result = rainbow_replace_ansi_colors(result, time_seconds);
634 if (rainbow_result) {
635 SAFE_FREE(result);
636 result = rainbow_result;
637 }
638
639 uint64_t t_color_replace_end = time_get_ns();
640 char color_replace_str[32];
641 time_pretty(t_color_replace_end - t_color_replace_start, -1, color_replace_str, sizeof(color_replace_str));
642 log_dev("COLOR_REPLACE: %s", color_replace_str);
643 }
644
645 // Apply digital rain effect if enabled
646 if (result && ctx->digital_rain) {
647 uint64_t t_rain_start = time_get_ns();
648 uint64_t current_time_ns = t_rain_start;
649 float delta_time = (float)(current_time_ns - ctx->last_frame_time_ns) / (float)NS_PER_SEC_INT;
650 ctx->last_frame_time_ns = current_time_ns;
651
652 // Update digital rain color from current filter (allows live filter changes)
653 color_filter_t current_filter = GET_OPTION(color_filter);
655
656 char *rain_result = digital_rain_apply(ctx->digital_rain, result, delta_time);
657 if (rain_result) {
658 SAFE_FREE(result);
659 result = rain_result;
660 }
661
662 uint64_t t_rain_end = time_get_ns();
663 char rain_str[32];
664 time_pretty(t_rain_end - t_rain_start, -1, rain_str, sizeof(rain_str));
665 log_dev("DIGITAL_RAIN: Effect applied (%s)", rain_str);
666 }
667
668 uint64_t t_cleanup_start = time_get_ns();
669 // Clean up flipped and filtered images if created
670 START_TIMER("ascii_convert_cleanup");
671 if (filtered_image) {
672 image_destroy(filtered_image);
673 }
674 if (flipped_image) {
675 image_destroy(flipped_image);
676 }
677 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 2 * NS_PER_MS_INT, "ascii_convert_cleanup",
678 "ASCII_CONVERT_CLEANUP: Cleanup complete (%.2f ms)");
679 uint64_t t_cleanup_end = time_get_ns();
680
681 // Log total breakdown with actual measured times
682 log_dev("CONVERT_TIMING: flip=%llu us, filter=%llu us, convert=%llu us, cleanup=%llu us, TOTAL=%llu us",
683 (t_flip_end - t_flip_start) / 1000, (t_filter_end - t_filter_start) / 1000,
684 (t_convert_end - t_convert_start) / 1000, (t_cleanup_end - t_cleanup_start) / 1000,
685 (t_cleanup_end - t_flip_start) / 1000);
686
687 return result;
688}
#define log_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
#define STOP_TIMER_AND_LOG_EVERY(log_level, interval_ns, threshold_ns, timer_name, msg_fmt,...)
Stop a timer and log the result with rate limiting.
Definition time.h:647
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as pretty duration with spaces and configurable precision.
Definition util/time.c:424
#define START_TIMER(name_fmt,...)
Start a timer with formatted name.
Definition time.h:333
#define NS_PER_MS_INT
Definition time.h:156
char * ascii_convert_with_capabilities(image_t *original, const ssize_t width, const ssize_t height, const terminal_capabilities_t *caps, const bool use_aspect_ratio, const bool stretch, const char *palette_chars)
Convert image to ASCII art with terminal capability awareness.
Definition ascii.c:233
char * rainbow_replace_ansi_colors(const char *ansi_string, float time_seconds)
Replace all RGB ANSI codes with rainbow color.
int apply_color_filter(uint8_t *pixels, uint32_t width, uint32_t height, uint32_t stride, color_filter_t filter, float time)
Apply color filter to entire image in-place.
void digital_rain_set_color_from_filter(digital_rain_t *rain, color_filter_t filter)
Set digital rain color from color filter.
void digital_rain_destroy(digital_rain_t *rain)
Destroy digital rain effect context.
char * digital_rain_apply(digital_rain_t *rain, const char *frame, float delta_time)
Apply digital rain effect to ASCII frame.
digital_rain_t * digital_rain_init(int num_columns, int num_rows)
Initialize digital rain effect context.
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
int w
Image width in pixels (must be > 0)
int h
Image height in pixels (must be > 0)
char palette_chars[256]
Palette character string for rendering.
digital_rain_t * digital_rain
Digital rain effect context (NULL if disabled)
uint64_t last_frame_time_ns
Last frame timestamp for digital rain delta time calculation.
terminal_capabilities_t caps
Detected terminal capabilities.
Complete terminal capabilities structure.
Definition terminal.h:709
terminal_color_mode_t color_level
Detected color support level (terminal_color_mode_t)
Definition terminal.h:711
render_mode_t render_mode
Preferred rendering mode (render_mode_t)
Definition terminal.h:721
@ COLOR_FILTER_NONE
No filtering (default)
Definition terminal.h:601
@ COLOR_FILTER_RAINBOW
Rainbow (cycles through spectrum over 3.5s)
Definition terminal.h:625
render_mode_t
Render mode preferences.
Definition terminal.h:662
terminal_color_mode_t
Terminal color support levels.
Definition terminal.h:578

References apply_color_filter(), ascii_convert_with_capabilities(), session_display_ctx::caps, COLOR_FILTER_NONE, COLOR_FILTER_RAINBOW, terminal_capabilities_t::color_level, session_display_ctx::digital_rain, digital_rain_apply(), digital_rain_destroy(), digital_rain_init(), digital_rain_set_color_from_filter(), ERROR_INVALID_PARAM, ERROR_INVALID_STATE, GET_OPTION, image_t::h, image_destroy(), image_new(), session_display_ctx::initialized, session_display_ctx::last_frame_time_ns, log_dev, log_info, NS_PER_MS_INT, NS_PER_SEC_INT, session_display_ctx::palette_chars, image_t::pixels, rainbow_replace_ansi_colors(), terminal_capabilities_t::render_mode, SAFE_FREE, SET_ERRNO, START_TIMER, STOP_TIMER_AND_LOG_EVERY, TERM_COLOR_AUTO, terminal_get_effective_height(), terminal_get_effective_width(), time_get_ns(), time_pretty(), and image_t::w.

Referenced by session_display_encode_frame(), session_pipeline_run_main(), and session_render_loop().

◆ session_display_create()

session_display_ctx_t * session_display_create ( const session_display_config_t *  config)

#include <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 166 of file src/common/session/display.c.

166 {
167 // Auto-create config from command-line options if NULL
168 session_display_config_t auto_config = {0};
169 if (!config) {
170 auto_config.snapshot_mode = GET_OPTION(snapshot_mode);
171 auto_config.palette_type = GET_OPTION(palette_type);
172 auto_config.custom_palette = GET_OPTION(palette_custom_set) ? GET_OPTION(palette_custom) : NULL;
173 auto_config.color_mode = TERM_COLOR_AUTO;
174 config = &auto_config;
175 }
176
177 // Check if we should exit before starting initialization
178 if (config->should_exit_callback && config->should_exit_callback(config->callback_data)) {
179 return NULL;
180 }
181
182 // Allocate context
184
185 // Store configuration
186 ctx->snapshot_mode = config->snapshot_mode;
187 ctx->palette_type = config->palette_type;
189 ctx->audio_ctx = config->audio_ctx;
190 ctx->render_fps = config->render_fps;
191 atomic_store_bool(&ctx->first_frame, true);
193
194 // Initialize FPS counter
196
197 // Get TTY info for direct terminal access
198 ctx->tty_info = get_current_tty();
199
200 // Determine if we have a valid TTY
201 // Check if stdout is a TTY, not just any fd (stdin/stderr could be TTY while stdout is piped).
202 // If stdout is piped/redirected, never perform terminal operations regardless of other fds.
203 if (ctx->tty_info.fd >= 0) {
205 }
206
207 // In piped mode, force all logs to stderr to prevent frame data corruption
210 // Disable buffering for stdout to ensure frames appear immediately
211 // Each frame ends with newline, so unbuffered output is critical for real-time display
212 (void)setvbuf(stdout, NULL, _IONBF, 0);
213 }
214
215 // Also disable buffering for TTY mode to ensure smooth 40+ FPS animation
216 setvbuf(stdout, NULL, _IONBF, 0);
217
218 // Detect terminal capabilities
220
221 // Set wants_padding based on terminal output mode
222 // Enable padding for all rendering modes (including snapshot) to center output
223 // The padding adds newlines at the top, which is useful for visual centering
224 bool is_snapshot_mode = config->snapshot_mode;
225 ctx->caps.wants_padding = true;
226
227 // Calculate pad_height: when padding is enabled, add top padding for centering
228 // Halfblock mode uses 2 source rows per output row, so padding is halved
229 if (ctx->caps.wants_padding) {
230 ctx->caps.pad_height = 5; // Halfblock needs half the padding (10 / 2 = 5)
231 } else {
232 ctx->caps.pad_height = 0;
233 }
234
235 log_debug("Padding mode: wants_padding=%d (snapshot=%d, has_tty=%d, stdin_tty=%d, stdout_tty=%d), pad_height=%zu",
236 ctx->caps.wants_padding, is_snapshot_mode, ctx->has_tty, terminal_is_stdin_tty(), terminal_is_stdout_tty(),
237 ctx->caps.pad_height);
238
239 // Apply color mode override if specified
240 if (config->color_mode != TERM_COLOR_AUTO) {
241 ctx->caps.color_level = config->color_mode;
242 }
243
244 // Apply command-line overrides
246
247 // Initialize palette
248 int palette_result = initialize_client_palette(config->palette_type, config->custom_palette, ctx->palette_chars,
249 &ctx->palette_len, ctx->luminance_palette);
250 if (palette_result != ASCIICHAT_OK) {
251 log_warn("Failed to initialize palette, using default");
252 // Fall back to standard palette
254 ctx->luminance_palette);
255 }
256
257 // Pre-warm UTF-8 palette cache during initialization (not during rendering)
258 // This is expensive on first use (creates lookup tables), so warm it up now to avoid frame lag
259 // This function is imported from lib/video/simd/common.h
260 (void)get_utf8_palette_cache((const char *)ctx->palette_chars);
261 log_debug("UTF-8 palette cache pre-warmed during display initialization");
262
263 // Initialize ANSI fast lookup tables based on terminal capabilities
266 } else if (ctx->caps.color_level == TERM_COLOR_256) {
268 } else if (ctx->caps.color_level == TERM_COLOR_16) {
270 }
271 // TERM_COLOR_MONO requires no init
272
273 // Initialize ASCII output system if we have a TTY
274 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
275 ascii_write_init(ctx->tty_info.fd, false);
276 }
277
278 // Initialize digital rain effect if enabled
279 if (GET_OPTION(matrix_rain)) {
280 // Get terminal dimensions for grid size
281 unsigned short int width_us = terminal_get_effective_width();
282 unsigned short int height_us = terminal_get_effective_height();
283
284 int width = (int)width_us;
285 int height = (int)height_us;
286
287 ctx->digital_rain = digital_rain_init(width, height);
288 if (ctx->digital_rain) {
289 // Set color from color filter if active
290 color_filter_t filter = GET_OPTION(color_filter);
292 log_info("Digital rain effect enabled: %dx%d grid", width, height);
293 } else {
294 log_warn("Failed to initialize digital rain effect");
295 }
297 }
298
299 // Initialize render-file unless media setup needs to determine its timing first.
300 const char *render_file_opt = GET_OPTION(render_file);
301 log_info("DISPLAY_CREATE: render-file opt='%s' (len=%zu), deferred=%d", render_file_opt ? render_file_opt : "(null)",
302 render_file_opt ? strlen(render_file_opt) : 0, config->defer_render_file);
303 if (!config->defer_render_file) {
305 if (render_err != ASCIICHAT_OK) {
306 log_error("render-file: Initialization failed with error %d", render_err);
307 }
308 }
309
310 ctx->initialized = true;
311 return ctx;
312}
void ansi_fast_init_256color(void)
Definition ansi.c:334
void ansi_fast_init(void)
Definition ansi.c:319
void ansi_fast_init_16color(void)
Definition ansi.c:398
void log_set_force_stderr(bool enabled)
Force all terminal log output to stderr.
Definition log/log.c:721
int initialize_client_palette(palette_type_t palette_type, const char *custom_chars, char client_palette_chars[256], size_t *client_palette_len, char client_luminance_palette[256])
Initialize client palette with full configuration.
Definition palette.c:299
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:83
terminal_capabilities_t apply_color_mode_override(terminal_capabilities_t caps)
Apply command-line overrides to detected capabilities.
Definition misc.c:29
tty_info_t get_current_tty(void)
Get current TTY information.
Definition misc.c:24
bool terminal_is_stdin_tty(void)
Check if stdin is connected to a TTY.
bool terminal_is_stdout_tty(void)
Check if stdout is connected to a TTY.
bool terminal_should_force_stderr(void)
Determine if logs should be forced to stderr.
terminal_capabilities_t detect_terminal_capabilities(void)
Detect terminal capabilities.
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)
fps_counter_t * fps_counter
FPS counter for measuring output throughput.
char luminance_palette[256]
Luminance-to-character mapping table (256 entries)
atomic_t first_frame
First frame flag for logging control.
void * audio_ctx
Audio context for playback (borrowed, not owned)
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.
bool wants_padding
Whether client wants frame padding (centering) - false for snapshot/piped modes.
Definition terminal.h:737
size_t pad_height
Number of top padding lines when wants_padding is true.
Definition terminal.h:739
@ TERM_COLOR_16
16-color support (standard ANSI colors)
Definition terminal.h:584
@ TERM_COLOR_256
256-color support (extended ANSI palette)
Definition terminal.h:586
@ TERM_COLOR_TRUECOLOR
24-bit truecolor support (RGB colors)
Definition terminal.h:588
int platform_isatty(int fd)
Check if a file descriptor is a terminal.
Definition util.c:63

References ansi_fast_init(), ansi_fast_init_16color(), ansi_fast_init_256color(), apply_color_mode_override(), ascii_write_init(), ASCIICHAT_OK, atomic_store_bool(), session_display_ctx::audio_ctx, session_display_config_t::audio_ctx, session_display_ctx::audio_playback_enabled, session_display_config_t::callback_data, session_display_ctx::caps, terminal_capabilities_t::color_level, session_display_config_t::color_mode, session_display_config_t::custom_palette, session_display_config_t::defer_render_file, detect_terminal_capabilities(), session_display_ctx::digital_rain, digital_rain_init(), digital_rain_set_color_from_filter(), session_display_config_t::enable_audio_playback, tty_info_t::fd, session_display_ctx::first_frame, session_display_ctx::fps_counter, fps_counter_create(), get_current_tty(), GET_OPTION, get_utf8_palette_cache(), session_display_ctx::has_tty, initialize_client_palette(), session_display_ctx::initialized, session_display_ctx::keyboard_help_active, session_display_ctx::last_frame_time_ns, log_debug, log_error, log_info, log_set_force_stderr(), log_warn, session_display_ctx::luminance_palette, terminal_capabilities_t::pad_height, session_display_ctx::palette_chars, session_display_ctx::palette_len, PALETTE_STANDARD, session_display_ctx::palette_type, session_display_config_t::palette_type, platform_isatty(), session_display_ctx::render_fps, session_display_config_t::render_fps, SAFE_CALLOC, session_display_init_render_file(), session_display_config_t::should_exit_callback, session_display_ctx::snapshot_mode, session_display_config_t::snapshot_mode, TERM_COLOR_16, TERM_COLOR_256, TERM_COLOR_AUTO, TERM_COLOR_TRUECOLOR, terminal_get_effective_height(), terminal_get_effective_width(), terminal_is_stdin_tty(), terminal_is_stdout_tty(), terminal_should_force_stderr(), time_get_ns(), session_display_ctx::tty_info, and terminal_capabilities_t::wants_padding.

Referenced by display_init(), and session_client_like_run().

◆ session_display_cursor_home()

void session_display_cursor_home ( session_display_ctx_t *  ctx)

#include <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 1082 of file src/common/session/display.c.

1082 {
1083 if (!ctx || !ctx->initialized) {
1084 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL or uninitialized");
1085 return;
1086 }
1087
1088 int fd = ctx->has_tty ? ctx->tty_info.fd : STDOUT_FILENO;
1089 if (fd >= 0) {
1090 (void)terminal_cursor_home(fd);
1091 }
1092}

References ERROR_INVALID_PARAM, tty_info_t::fd, session_display_ctx::has_tty, session_display_ctx::initialized, SET_ERRNO, terminal_cursor_home(), and session_display_ctx::tty_info.

◆ session_display_destroy()

void session_display_destroy ( session_display_ctx_t *  ctx)

#include <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 314 of file src/common/session/display.c.

314 {
315 if (!ctx) {
316 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL");
317 return;
318 }
319
320 // Cleanup ASCII rendering if we had a TTY
321 // Don't reset terminal in snapshot mode to preserve the rendered output
322 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
324 }
325
326 // Close the controlling terminal if we opened it
327 if (ctx->tty_info.owns_fd && ctx->tty_info.fd >= 0) {
328 (void)platform_close(ctx->tty_info.fd);
329 ctx->tty_info.fd = -1;
330 ctx->tty_info.owns_fd = false;
331 }
332
333 // Cleanup digital rain effect
334 if (ctx->digital_rain) {
336 ctx->digital_rain = NULL;
337 }
338
339 // Cleanup FPS counter
340 if (ctx->fps_counter) {
342 ctx->fps_counter = NULL;
343 }
344
345 // Cleanup render-file if active
346 if (ctx->render_file) {
348 ctx->render_file = NULL;
349 }
350
351 ctx->initialized = false;
352
353 // Clear the cached display context in splash state to prevent use-after-free
354 // when worker threads try to access the display after it's been freed
356
357 SAFE_FREE(ctx);
358}
int platform_close(int fd)
Safe file close (close replacement)
void splash_clear_display_context(void)
Clear the cached display context to prevent use-after-free.
Definition splash.c:721
void ascii_write_destroy(int fd, bool reset_terminal)
Destroy ASCII write subsystem.
Definition ascii.c:442
asciichat_error_t render_file_destroy(render_file_ctx_t *ctx)
Definition renderer.c:247
render_file_ctx_t * render_file
Render-to-file context (NULL if disabled)
bool owns_fd
True if we opened the FD and should close it, false otherwise.
Definition terminal.h:759

References ascii_write_destroy(), session_display_ctx::digital_rain, digital_rain_destroy(), ERROR_INVALID_PARAM, tty_info_t::fd, session_display_ctx::fps_counter, fps_counter_destroy(), session_display_ctx::has_tty, session_display_ctx::initialized, tty_info_t::owns_fd, platform_close(), session_display_ctx::render_file, render_file_destroy(), SAFE_FREE, SET_ERRNO, session_display_ctx::snapshot_mode, splash_clear_display_context(), and session_display_ctx::tty_info.

Referenced by session_client_like_run().

◆ session_display_encode_frame()

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

#include <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 945 of file src/common/session/display.c.

945 {
946 static int call_count = 0;
947 if (call_count++ < 5) {
948 log_info("session_display_encode_frame: CALLED (ctx=%p, image=%p, captured_ns=%llu, ctx->render_file=%p)",
949 (void *)ctx, (void *)image, (unsigned long long)captured_ns, ctx ? (void *)ctx->render_file : NULL);
950 }
951
952 if (!ctx || !ctx->initialized) {
953 SET_ERRNO(ERROR_INVALID_PARAM, "Display context is NULL or uninitialized");
954 return;
955 }
956
957 if (!image) {
958 SET_ERRNO(ERROR_INVALID_PARAM, "Image is NULL");
959 return;
960 }
961
962 // Only encode if render_file is active
963 if (!ctx->render_file) {
964 log_warn_every(10 * NS_PER_SEC_INT, "session_display_encode_frame: render_file is NULL, skipping encode");
965 return;
966 }
967
968 // Convert image to ASCII
969 char *ascii = session_display_convert_to_ascii(ctx, image);
970 if (!ascii) {
971 log_warn("session_display_encode_frame: Failed to convert image to ASCII");
972 return;
973 }
974
975 // Write the same selected visualization used by the terminal to the render-file encoder.
976 char *visualization_frame = NULL;
977 const char *render_frame = ascii;
978 if (GET_OPTION(waveform) || GET_OPTION(fft)) {
979 visualization_frame = session_display_create_visualization_frame(ctx);
980 if (visualization_frame)
981 render_frame = visualization_frame;
982 }
983
984 asciichat_error_t fe = render_file_write_frame(ctx->render_file, render_frame, captured_ns);
985 SAFE_FREE(visualization_frame);
986 if (fe != ASCIICHAT_OK) {
987 log_warn_every(5 * NS_PER_SEC_INT, "render-file: encode failed (%s)", asciichat_error_string(fe));
988 }
989
990 SAFE_FREE(ascii);
991}
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.
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:708
asciichat_error_t render_file_write_frame(render_file_ctx_t *ctx, const char *ansi_frame, uint64_t captured_ns)
Definition renderer.c:146

References ASCIICHAT_OK, ERROR_INVALID_PARAM, GET_OPTION, session_display_ctx::initialized, log_info, log_warn, log_warn_every, NS_PER_SEC_INT, session_display_ctx::render_file, render_file_write_frame(), SAFE_FREE, session_display_convert_to_ascii(), and SET_ERRNO.

◆ session_display_get_caps()

const terminal_capabilities_t * session_display_get_caps ( session_display_ctx_t *  ctx)

#include <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 382 of file src/common/session/display.c.

382 {
383 if (!ctx || !ctx->initialized) {
384 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
385 return NULL;
386 }
387 return &ctx->caps;
388}

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 <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 1191 of file src/common/session/display.c.

1191 {
1192 return get_current_display_ctx();
1193}

Referenced by session_client_like_run().

◆ session_display_get_luminance_palette()

const char * session_display_get_luminance_palette ( session_display_ctx_t *  ctx)

#include <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 406 of file src/common/session/display.c.

406 {
407 if (!ctx || !ctx->initialized) {
408 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
409 return NULL;
410 }
411 return ctx->luminance_palette;
412}

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 <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 390 of file src/common/session/display.c.

390 {
391 if (!ctx || !ctx->initialized) {
392 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
393 return NULL;
394 }
395 return ctx->palette_chars;
396}

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 <display.h>

Get the palette character count.

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

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

398 {
399 if (!ctx || !ctx->initialized) {
400 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
401 return 0;
402 }
403 return ctx->palette_len;
404}

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 <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 431 of file src/common/session/display.c.

431 {
432 if (!ctx)
433 return 0;
434 return ctx->render_fps;
435}

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 <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 422 of file src/common/session/display.c.

422 {
423 if (!ctx || !ctx->initialized) {
424 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
425 return NULL;
426 }
427
428 return ctx->stdin_reader;
429}
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 <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 414 of file src/common/session/display.c.

414 {
415 if (!ctx || !ctx->initialized) {
416 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
417 return -1;
418 }
419 return ctx->tty_info.fd;
420}

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

Referenced by keyboard_help_render().

◆ session_display_has_audio_playback()

bool session_display_has_audio_playback ( session_display_ctx_t *  ctx)

#include <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 1094 of file src/common/session/display.c.

1094 {
1095 if (!ctx || !ctx->initialized) {
1096 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL or uninitialized");
1097 return false;
1098 }
1099 return ctx->audio_playback_enabled;
1100}

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 <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 446 of file src/common/session/display.c.

446 {
447 if (!ctx) {
448 return false;
449 }
450 // Return true if first_frame flag is false (meaning first frame has been rendered)
451 return !atomic_load_bool(&ctx->first_frame);
452}

References atomic_load_bool(), and session_display_ctx::first_frame.

◆ session_display_has_render_file()

bool session_display_has_render_file ( session_display_ctx_t *  ctx)

#include <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 462 of file src/common/session/display.c.

462 {
463 if (!ctx) {
464 return false;
465 }
466 return ctx->render_file != NULL;
467}

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 <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 374 of file src/common/session/display.c.

374 {
375 if (!ctx || !ctx->initialized) {
376 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
377 return false;
378 }
379 return ctx->has_tty;
380}

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

Referenced by display_has_tty(), and keyboard_help_render().

◆ session_display_render_fps_overlay()

void session_display_render_fps_overlay ( session_display_ctx_t *  ctx)

#include <display.h>

Render FPS counter overlay in top-right corner.

Parameters
ctxDisplay context (must not be NULL)

Displays the current FPS value as a reverse-video overlay in the top-right corner of the terminal. Only renders in TTY mode. Safe to call when fps_counter is NULL.

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

1013 {
1014 if (!ctx || !ctx->fps_counter || !ctx->initialized) {
1015 return;
1016 }
1017
1018 // Only render overlay in TTY mode
1019 if (!ctx->has_tty) {
1020 return;
1021 }
1022
1023 // Get current FPS value from counter
1024 float fps = fps_counter_get(ctx->fps_counter);
1025
1026 // Get terminal width for right-alignment
1027 int term_cols = (int)terminal_get_effective_width();
1028
1029 // Position at row 1, right side of screen
1030 // Column = term_cols - 7 (to fit "FPS:144" which is 7 chars)
1031 // Use reverse video (\033[7m) for visibility, then reset (\033[0m)
1032 char overlay[64];
1033 int overlay_len = snprintf(overlay, sizeof(overlay), "\033[1;%dH\033[7mFPS:%3.0f\033[0m", term_cols - 6, fps);
1034
1035 if (overlay_len > 0 && overlay_len < (int)sizeof(overlay)) {
1036 // Write overlay to TTY
1037 int fd = (ctx->has_tty && ctx->tty_info.fd >= 0) ? ctx->tty_info.fd : STDOUT_FILENO;
1038 (void)platform_write_all(fd, overlay, overlay_len);
1039
1040 // Flush to ensure overlay appears immediately
1041 (void)terminal_flush(fd);
1042 }
1043}

References tty_info_t::fd, session_display_ctx::fps_counter, fps_counter_get(), session_display_ctx::has_tty, session_display_ctx::initialized, platform_write_all(), terminal_flush(), terminal_get_effective_width(), and session_display_ctx::tty_info.

Referenced by session_display_write_ascii().

◆ session_display_render_frame()

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

#include <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 718 of file src/common/session/display.c.

718 {
719 // Legacy function: shim that calls both new split functions for backward compatibility
720 // This maintains API compatibility with existing code while delegating to specialized functions
721
722 if (!ctx || !frame_data) {
723 SET_ERRNO(ERROR_INVALID_PARAM, "Display context or frame data is NULL");
724 return;
725 }
726
727 // Write the selected visualization to the render file as well as the terminal.
728 char *visualization_frame = NULL;
729 const char *render_frame = frame_data;
730 if (GET_OPTION(waveform) || GET_OPTION(fft)) {
731 visualization_frame = session_display_create_visualization_frame(ctx);
732 if (visualization_frame)
733 render_frame = visualization_frame;
734 }
735
736 // Write ASCII to terminal (main thread output path)
737 if (ctx && ctx->render_file) {
740 if (err != ASCIICHAT_OK)
741 log_warn_every(5 * NS_PER_SEC_INT, "Recording received frame failed: %s", asciichat_error_string(err));
742 }
743 SAFE_FREE(visualization_frame);
744 session_display_write_ascii(ctx, frame_data);
745}
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:139

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

Referenced by display_render_frame(), and session_render_loop().

◆ session_display_reset()

void session_display_reset ( session_display_ctx_t *  ctx)

#include <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 1045 of file src/common/session/display.c.

1045 {
1046 if (!ctx || !ctx->initialized) {
1047 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL or uninitialized");
1048 return;
1049 }
1050
1051 // Skip terminal reset in snapshot mode to preserve rendered output
1052 if (ctx->snapshot_mode) {
1053 return;
1054 }
1055
1056 // Only perform terminal operations if we have a valid TTY
1057 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
1058 (void)terminal_reset(ctx->tty_info.fd);
1059 (void)terminal_cursor_show();
1060 (void)terminal_flush(ctx->tty_info.fd);
1061 }
1062}
asciichat_error_t terminal_reset(int fd)
Reset terminal to default state.
Definition misc.c:81
asciichat_error_t terminal_cursor_show(void)
Show terminal cursor.

References ERROR_INVALID_PARAM, tty_info_t::fd, session_display_ctx::has_tty, session_display_ctx::initialized, SET_ERRNO, session_display_ctx::snapshot_mode, terminal_cursor_show(), terminal_flush(), terminal_reset(), and session_display_ctx::tty_info.

Referenced by display_full_reset().

◆ session_display_reset_first_frame()

void session_display_reset_first_frame ( session_display_ctx_t *  ctx)

#include <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 454 of file src/common/session/display.c.

454 {
455 if (!ctx) {
456 return;
457 }
458 // Reset first_frame flag to true so splash screen can be shown again on next render
459 atomic_store_bool(&ctx->first_frame, true);
460}

References atomic_store_bool(), and session_display_ctx::first_frame.

Referenced by session_client_like_run().

◆ session_display_set_global_context()

void session_display_set_global_context ( session_display_ctx_t *  ctx)

#include <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 1186 of file src/common/session/display.c.

1186 {
1187 atomic_ptr_store(&g_current_display_ctx, (void *)ctx);
1188}
void atomic_ptr_store(atomic_ptr_t *a, void *value)
Atomically store a pointer.
Definition atomic.c:280

References atomic_ptr_store().

Referenced by session_client_like_run(), 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 <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 1196 of file src/common/session/display.c.

1196 {
1198}
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 <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 437 of file src/common/session/display.c.

437 {
438 if (!ctx)
439 return;
440 ctx->render_fps = fps;
441 if (fps > 0) {
442 log_debug("session_display_set_render_fps: Updated render FPS to %u (for correct video duration)", fps);
443 }
444}

References log_debug, and session_display_ctx::render_fps.

Referenced by session_client_like_run().

◆ session_display_set_snapshot_actual_duration()

void session_display_set_snapshot_actual_duration ( session_display_ctx_t *  ctx,
double  actual_duration_sec 
)

#include <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 1247 of file src/common/session/display.c.

1247 {
1248 log_info("session_display_set_snapshot_actual_duration: CALLED with duration=%.3f, ctx=%p, render_file=%p",
1249 actual_duration_sec, (void *)ctx, ctx ? (void *)ctx->render_file : NULL);
1250
1251 if (!ctx) {
1252 SET_ERRNO(ERROR_INVALID_PARAM, "Session display context is NULL");
1253 return;
1254 }
1255
1256 if (!ctx->render_file) {
1257 log_warn(" -> render_file is NULL, cannot set duration");
1258 return;
1259 }
1260
1261 log_info(" -> Passing duration %.3f to render_file encoder", actual_duration_sec);
1262 render_file_set_snapshot_actual_duration(ctx->render_file, actual_duration_sec);
1263}
void render_file_set_snapshot_actual_duration(render_file_ctx_t *ctx, double actual_duration_sec)
Definition renderer.c:241

References ERROR_INVALID_PARAM, log_info, log_warn, 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 <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 360 of file src/common/session/display.c.

360 {
361 if (!ctx) {
362 SET_ERRNO(ERROR_INVALID_PARAM, "Display context is NULL");
363 return;
364 }
365
366 ctx->stdin_reader = (terminal_fd_reader_t *)reader;
367 log_debug("session_display: stdin_reader set");
368}

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

Referenced by session_client_like_run().

◆ session_display_write_ascii()

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

#include <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 747 of file src/common/session/display.c.

747 {
748 if (!ctx || !ctx->initialized) {
749 SET_ERRNO(ERROR_INVALID_PARAM, "Display context is NULL or uninitialized");
750 return;
751 }
752
753 if (!ascii) {
754 SET_ERRNO(ERROR_INVALID_PARAM, "ASCII data is NULL");
755 return;
756 }
757
758 // Re-evaluate color_level on every frame to pick up live color_mode changes
760 if (color_mode != TERM_COLOR_AUTO) {
761 ctx->caps.color_level = color_mode;
762 }
763
764 // Suppress frame rendering when help screen is active
766 return;
767 }
768
769 bool visualization_enabled = GET_OPTION(waveform) || GET_OPTION(fft);
770 char *visualization_frame = visualization_enabled ? session_display_create_visualization_frame(ctx) : NULL;
771 if (visualization_frame)
772 ascii = visualization_frame;
773
774 // Calculate frame length
775 size_t frame_len = strnlen(ascii, 1024 * 1024); // Max 1MB frame
776 if (frame_len == 0) {
777 SET_ERRNO(ERROR_INVALID_PARAM, "ASCII data is empty");
778 SAFE_FREE(visualization_frame);
779 return;
780 }
781
782 // Apply digital rain effect if enabled
783 char *display_frame = (char *)ascii;
784 char *rain_result = NULL;
785 if (ctx->digital_rain && !visualization_enabled) {
786 uint64_t t_rain_start = time_get_ns();
787 uint64_t current_time_ns = t_rain_start;
788 float delta_time = (float)(current_time_ns - ctx->last_frame_time_ns) / (float)NS_PER_SEC_INT;
789 ctx->last_frame_time_ns = current_time_ns;
790
791 // Update digital rain color from current filter
792 color_filter_t current_filter = GET_OPTION(color_filter);
794
795 rain_result = digital_rain_apply(ctx->digital_rain, (char *)ascii, delta_time);
796 if (rain_result) {
797 display_frame = rain_result;
798 frame_len = strnlen(rain_result, 1024 * 1024);
799
800 uint64_t t_rain_end = time_get_ns();
801 char rain_str[32];
802 time_pretty(t_rain_end - t_rain_start, -1, rain_str, sizeof(rain_str));
803 log_info("DIGITAL_RAIN (write_ascii): Effect applied (%s)", rain_str);
804 }
805 }
806
807 // Handle first frame - perform initial terminal reset and splash cleanup
808 bool was_first_frame = atomic_exchange_bool(&ctx->first_frame, false);
809 if (was_first_frame) {
810 // Stop splash screen when first frame is ready
812
813 // Perform initial terminal reset
814 if (ctx->has_tty && !ctx->render_file) {
815 (void)terminal_reset(STDOUT_FILENO);
816 (void)terminal_clear_screen();
817 (void)terminal_cursor_home(STDOUT_FILENO);
818 (void)terminal_clear_scrollback(STDOUT_FILENO);
819 (void)terminal_cursor_show();
820 if (!ctx->snapshot_mode) {
821 (void)terminal_cursor_hide();
822 }
823 (void)terminal_flush(STDOUT_FILENO);
824 }
825 }
826
827 // Output routing logic
828 bool use_tty_control = ctx->has_tty && (!ctx->snapshot_mode || terminal_is_interactive());
829
830 START_TIMER("frame_write");
831
832 // Increment shared frame counter for both client and host rendering
833 // This metric is used by debug scripts to measure FPS across all modes
834 static atomic_t g_display_frames_rendered = {0};
835 int frame_number = atomic_fetch_add_u64(&g_display_frames_rendered, 1) + 1;
836 log_dev("[FRAMES_RENDERED_TOTAL] %d", frame_number);
837
838 if (use_tty_control) {
839 // TTY mode: Buffer cursor control + frame data together for atomic frame display
840 const char *cursor_home_sequence = "\033[H\033[3J"; // 7 bytes total
841 size_t cursor_seq_len = 7;
842 size_t total_size = cursor_seq_len + frame_len;
843
844 char *frame_buffer = SAFE_MALLOC(total_size, char *);
845 if (frame_buffer) {
846 memcpy(frame_buffer, cursor_home_sequence, cursor_seq_len);
847 memcpy(frame_buffer + cursor_seq_len, display_frame, frame_len);
848
849 log_debug("FRAME_WRITE_TTY: Writing %zu bytes (cursor=%zu + frame=%zu) to stdout", total_size, cursor_seq_len,
850 frame_len);
851 ssize_t written = platform_write_all(STDOUT_FILENO, frame_buffer, total_size);
852 log_debug("FRAME_WRITE_TTY: Wrote %zd bytes (requested %zu)", written, total_size);
853
854 // Start snapshot timer on first ASCII frame rendered
855 if (GET_OPTION(snapshot_mode) && !g_snapshot_first_frame_rendered) {
858 log_info("SNAPSHOT: FIRST ASCII FRAME RENDERED (write_ascii) - Timer started");
859 }
860
861 // Tick FPS counter
862 if (ctx->fps_counter) {
864 }
865
867 }
868
869 // Flush buffers
870 log_debug("FRAME_FLUSH: Flushing stdout");
871 (void)fflush(stdout);
872 int flush_fd = (ctx->tty_info.fd >= 0) ? ctx->tty_info.fd : STDOUT_FILENO;
873 (void)terminal_flush(flush_fd);
874
875 // Render FPS counter overlay if enabled
876 if (ctx->fps_counter && GET_OPTION(fps_counter)) {
878 }
879 } else if (terminal_is_interactive()) {
880 // Piped to interactive terminal: output ASCII frames with newline
881 char *write_buf = SAFE_MALLOC(frame_len + 1, char *);
882 if (write_buf) {
883 memcpy(write_buf, display_frame, frame_len);
884 write_buf[frame_len] = '\n';
885 (void)platform_write_all(STDOUT_FILENO, write_buf, frame_len + 1);
886 SAFE_FREE(write_buf);
887 }
888
889 // Flush buffers
890 (void)fflush(stdout);
891 int flush_fd = (ctx->tty_info.fd >= 0) ? ctx->tty_info.fd : STDOUT_FILENO;
892 (void)terminal_flush(flush_fd);
893 } else {
894 // Non-interactive piped output
895 // BUT: Don't write ASCII frames to stdout if render_file is using stdout (--render-file="-")
896 if (!ctx->render_file) {
897 // No render-file, safe to write ASCII frames to stdout
898 char *write_buf = SAFE_MALLOC(frame_len + 1, char *);
899 if (write_buf) {
900 memcpy(write_buf, display_frame, frame_len);
901 write_buf[frame_len] = '\n';
902 (void)platform_write_all(STDOUT_FILENO, write_buf, frame_len + 1);
903
904 // Start snapshot timer on first ASCII frame rendered
905 if (GET_OPTION(snapshot_mode) && !g_snapshot_first_frame_rendered) {
908 log_info("SNAPSHOT: FIRST ASCII FRAME RENDERED (write_ascii piped) - Timer started");
909 }
910
911 SAFE_FREE(write_buf);
912 }
913
914 // Flush buffers (skip terminal_flush on pipes to avoid blocking in snapshot mode)
915 (void)fflush(stdout);
916 if (!GET_OPTION(snapshot_mode) || ctx->has_tty) {
917 int flush_fd = (ctx->tty_info.fd >= 0) ? ctx->tty_info.fd : STDOUT_FILENO;
918 (void)terminal_flush(flush_fd);
919 }
920 } else {
921 // render_file is using stdout: skip ASCII output to avoid mixing with video
922 if (GET_OPTION(snapshot_mode) && !g_snapshot_first_frame_rendered) {
925 log_info("SNAPSHOT: FIRST FRAME RENDERED (render_file piped) - Timer started");
926 }
927 }
928 }
929
930 STOP_TIMER_AND_LOG_EVERY(dev, 3 * NS_PER_SEC_INT, 5 * NS_PER_MS_INT, "frame_write",
931 "FRAME_WRITE: Write and flush complete (%.2f ms)");
932
933 // Clean up digital rain result if allocated
934 if (rain_result) {
935 SAFE_FREE(rain_result);
936 }
937 SAFE_FREE(visualization_frame);
938}
uint64_t atomic_fetch_add_u64(atomic_t *a, uint64_t delta)
Atomically add to a uint64_t and return the previous value.
Definition atomic.c:248
bool atomic_exchange_bool(atomic_t *a, bool new_value)
Atomically exchange a boolean and return the old value.
Definition atomic.c:303
uint64_t g_snapshot_first_frame_rendered_ns
asciichat_error_t terminal_clear_scrollback(int fd)
Clear terminal scrollback buffer.
Definition misc.c:76
asciichat_error_t terminal_cursor_hide(void)
Hide terminal cursor.
int splash_intro_done(void)
Signal that intro splash should end (first frame ready to render)
Definition splash.c:693
void session_display_render_fps_overlay(session_display_ctx_t *ctx)
Render FPS counter overlay in top-right corner.
bool g_snapshot_first_frame_rendered
bool terminal_is_interactive(void)
Check if the session is fully interactive.
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
Frame buffer structure.

References atomic_exchange_bool(), atomic_fetch_add_u64(), atomic_load_bool(), session_display_ctx::caps, terminal_capabilities_t::color_level, session_display_ctx::digital_rain, digital_rain_apply(), digital_rain_set_color_from_filter(), ERROR_INVALID_PARAM, tty_info_t::fd, session_display_ctx::first_frame, session_display_ctx::fps_counter, fps_counter_tick(), g_snapshot_first_frame_rendered, g_snapshot_first_frame_rendered_ns, GET_OPTION, session_display_ctx::has_tty, session_display_ctx::initialized, session_display_ctx::keyboard_help_active, session_display_ctx::last_frame_time_ns, log_debug, log_dev, log_info, NS_PER_MS_INT, NS_PER_SEC_INT, platform_write_all(), session_display_ctx::render_file, SAFE_FREE, SAFE_MALLOC, session_display_render_fps_overlay(), SET_ERRNO, session_display_ctx::snapshot_mode, splash_intro_done(), START_TIMER, STOP_TIMER_AND_LOG_EVERY, TERM_COLOR_AUTO, terminal_clear_screen(), terminal_clear_scrollback(), terminal_cursor_hide(), terminal_cursor_home(), terminal_cursor_show(), terminal_flush(), terminal_is_interactive(), terminal_reset(), time_get_ns(), time_pretty(), and session_display_ctx::tty_info.

Referenced by session_display_render_frame(), and session_pipeline_run_main().

◆ session_display_write_audio()

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

#include <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 1102 of file src/common/session/display.c.

1102 {
1103 if (!ctx || !ctx->initialized || !buffer || num_samples == 0) {
1104 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, buffer=%p, num_samples=%zu", ctx, buffer,
1105 num_samples);
1106 }
1107
1108 if (!ctx->audio_playback_enabled || !ctx->audio_ctx) {
1109 // Audio not enabled, but not an error - just skip silently
1110 return ASCIICHAT_OK;
1111 }
1112
1113 // For mirror mode with local files: write samples directly without jitter buffering
1114 // The jitter buffer is designed for network scenarios with irregular packet arrivals
1115 // For local playback, we just want raw samples flowing to the speakers
1116 audio_context_t *audio_ctx = (audio_context_t *)ctx->audio_ctx;
1117 if (audio_ctx && audio_ctx->playback_buffer) {
1118 audio_ring_buffer_t *rb = audio_ctx->playback_buffer;
1119
1120 // Simple direct write: append samples to ring buffer without network-oriented complexity
1121 uint32_t write_idx = (uint32_t)atomic_load_u64(&rb->write_index);
1122 uint32_t read_idx = (uint32_t)atomic_load_u64(&rb->read_index);
1123
1124 // Calculate available space in ring buffer
1125 uint32_t available = (read_idx - write_idx - 1) & (AUDIO_RING_BUFFER_SIZE - 1);
1126
1127 if ((uint32_t)num_samples > available) {
1128 // Buffer full - just skip this write to avoid distortion from overwriting old audio
1129 return ASCIICHAT_OK;
1130 }
1131
1132 // Direct memcpy to ring buffer, handling wrap-around
1133 uint32_t space_before_wrap = AUDIO_RING_BUFFER_SIZE - write_idx;
1134 if ((uint32_t)num_samples <= space_before_wrap) {
1135 memcpy(&rb->data[write_idx], buffer, num_samples * sizeof(float));
1136 } else {
1137 // Split write: first part to end of buffer, second part wraps to beginning
1138 uint32_t first_part = space_before_wrap;
1139 uint32_t second_part = num_samples - first_part;
1140 memcpy(&rb->data[write_idx], buffer, first_part * sizeof(float));
1141 memcpy(&rb->data[0], &buffer[first_part], second_part * sizeof(float));
1142 }
1143
1144 // Update write index atomically
1145 atomic_store_u64(&rb->write_index, (write_idx + num_samples) % AUDIO_RING_BUFFER_SIZE);
1146
1147 return ASCIICHAT_OK;
1148 }
1149
1150 return ASCIICHAT_OK;
1151}
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233
#define AUDIO_RING_BUFFER_SIZE
Audio ring buffer size in samples (192000 samples = 4 seconds @ 48kHz)
Definition ringbuffer.h:128
Audio ring buffer for real-time audio streaming.
Definition ringbuffer.h:205
atomic_t read_index
Read index (consumer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:211
atomic_t write_index
Write index (producer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:209
float data[192000]
Audio sample data buffer.
Definition ringbuffer.h:207

References ASCIICHAT_OK, atomic_load_u64(), atomic_store_u64(), 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 <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 993 of file src/common/session/display.c.

993 {
994 if (!ctx || !ctx->initialized || !data || len == 0) {
995 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, data=%p, len=%zu", ctx, data, len);
996 return;
997 }
998
999 int fd = -1;
1000 if (ctx->has_tty && ctx->tty_info.fd >= 0) {
1001 fd = ctx->tty_info.fd;
1002 } else {
1003 fd = STDOUT_FILENO;
1004 }
1005
1006 // Write all data with automatic retry on transient errors
1007 (void)platform_write_all(fd, data, len);
1008
1009 // Flush immediately after write to TTY to ensure data is sent
1010 (void)terminal_flush(fd);
1011}

References ERROR_INVALID_PARAM, tty_info_t::fd, session_display_ctx::has_tty, session_display_ctx::initialized, platform_write_all(), SET_ERRNO, terminal_flush(), and session_display_ctx::tty_info.

Referenced by keyboard_help_render(), and update_banner_show_prompt().

◆ session_handle_keyboard_input()

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

#include <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
  • 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 104 of file keyboard_handler.c.

104 {
105 // Debug: log all key codes to help identify unknown keys
106 if (key != KEY_NONE) {
107 log_info("KEYBOARD INPUT: code=%d (0x%02x) char='%c'", key, key, (key >= 32 && key < 127) ? key : '?');
108 }
109
110 switch ((int)key) {
111 // ===== HELP SCREEN TOGGLE =====
112 case KEY_QUESTION: {
113 log_info("✓✓✓ USER PRESSED ? KEY - HELP SCREEN TOGGLE ✓✓✓");
114 if (display) {
115 log_info("Toggling help screen (display=%p)", (void *)display);
116 keyboard_help_toggle(display);
117 // Render help screen immediately so user sees it
118 keyboard_help_render(display);
119 log_info("✓ Help screen toggle complete");
120 } else {
121 log_error("ERROR: Cannot toggle help - display context is NULL");
122 }
123 break;
124 }
125
126 // ===== HELP SCREEN CLOSE / QUIT =====
127 case KEY_ESCAPE: {
128 if (display && keyboard_help_is_active(display)) {
129 // Close help screen if it's active
130 keyboard_help_toggle(display);
132 } else {
133 // If help screen is not active, quit the app (like Ctrl-C)
134 // The signal handler will gracefully shutdown all modes (client, server, mirror, etc.)
135 raise(SIGINT);
136 }
137 break;
138 }
139
140 // ===== SEEK CONTROLS (file sources only) =====
141 case KEY_LEFT: { // Seek backward 30 seconds
142 if (capture) {
144 if (source && media_source_get_type(source) == MEDIA_SOURCE_FILE) {
145 double current_pos = media_source_get_position(source);
146 if (current_pos >= 0.0) {
147 double new_pos = current_pos - 30.0;
148 if (new_pos < 0.0) {
149 new_pos = 0.0;
150 }
151 asciichat_error_t err = media_source_seek(source, new_pos);
152 if (err == ASCIICHAT_OK) {
153 log_info("Seeked backward to %.1f seconds", new_pos);
154 }
155 void *audio_ctx = session_capture_get_audio_context(capture);
156 if (audio_ctx) {
158 }
159 }
160 }
161 }
162 break;
163 }
164
165 case KEY_RIGHT: { // Seek forward 30 seconds
166 if (capture) {
168 if (source && media_source_get_type(source) == MEDIA_SOURCE_FILE) {
169 double current_pos = media_source_get_position(source);
170 double duration = media_source_get_duration(source);
171 if (current_pos >= 0.0) {
172 double new_pos = current_pos + 30.0;
173 // Clamp to duration if known
174 if (duration > 0.0 && new_pos > duration) {
175 new_pos = duration;
176 }
177 asciichat_error_t err = media_source_seek(source, new_pos);
178 if (err == ASCIICHAT_OK) {
179 log_info("Seeked forward to %.1f seconds", new_pos);
180 }
181 void *audio_ctx = session_capture_get_audio_context(capture);
182 if (audio_ctx) {
184 }
185 }
186 }
187 }
188 break;
189 }
190
191 // ===== VOLUME CONTROLS =====
192 case KEY_DOWN: { // Decrease volume 10%
193 double current_volume = GET_OPTION(speakers_volume);
194 double new_volume = clamp_volume(current_volume - 0.1);
195 options_set_double("speakers_volume", new_volume);
196 double verify_volume = GET_OPTION(speakers_volume);
197 log_info("Volume DOWN: %.0f%% → %.0f%% (verified: %.0f%%)", current_volume * 100.0, new_volume * 100.0,
198 verify_volume * 100.0);
199 break;
200 }
201
202 case KEY_UP: { // Increase volume 10%
203 double current_volume = GET_OPTION(speakers_volume);
204 double new_volume = clamp_volume(current_volume + 0.1);
205 options_set_double("speakers_volume", new_volume);
206 double verify_volume = GET_OPTION(speakers_volume);
207 log_info("Volume UP: %.0f%% → %.0f%% (verified: %.0f%%)", current_volume * 100.0, new_volume * 100.0,
208 verify_volume * 100.0);
209 break;
210 }
211
212 // ===== PLAY/PAUSE CONTROL =====
213 case KEY_SPACE: { // Toggle play/pause
214 if (capture) {
216 if (source && media_source_get_type(source) == MEDIA_SOURCE_FILE) {
218 if (media_source_is_paused(source)) {
219 log_info("Paused");
220 } else {
221 log_info("Playing");
222 }
223 void *audio_ctx = session_capture_get_audio_context(capture);
224 if (audio_ctx) {
226 }
227 }
228 }
229 break;
230 }
231
232 // ===== COLOR MODE CONTROL =====
233 case KEY_C:
234 case 'C': {
235 int current_mode = (int)GET_OPTION(color_mode);
236 int next_mode = next_color_mode(current_mode);
237 options_set_int("color_mode", next_mode);
238
239 const char *mode_names[] = {"Mono", "16-color", "256-color", "Truecolor"};
240 if (next_mode >= 0 && next_mode <= 3) {
241 log_info("Color mode: %s", mode_names[next_mode]);
242 }
243 break;
244 }
245
246 // ===== MUTE CONTROL =====
247 case KEY_M:
248 case 'M': {
249 double current_volume = GET_OPTION(speakers_volume);
250 log_debug("Mute toggle: current_volume=%.2f, g_mute_saved_volume=%.2f, threshold=0.01", current_volume,
251 g_mute_saved_volume);
252
253 if (current_volume > 0.01) { // If not already muted
254 // Save current volume and mute
255 g_mute_saved_volume = current_volume;
256 options_set_double("speakers_volume", 0.0);
257 double verify = GET_OPTION(speakers_volume);
258 log_info("Muted: saved %.0f%%, set to 0%% (verified: %.2f)", g_mute_saved_volume * 100.0, verify);
259 } else {
260 // Restore previous volume
261 double restore_volume = g_mute_saved_volume > 0.0 ? g_mute_saved_volume : 0.5;
262 options_set_double("speakers_volume", restore_volume);
263 double verify = GET_OPTION(speakers_volume);
264 log_info("Unmuted: restored %.0f%% (verified: %.2f)", restore_volume * 100.0, verify);
265 }
266 break;
267 }
268
269 // ===== RENDER MODE CONTROL =====
270 case KEY_R:
271 case 'R': {
272 int current_mode = (int)GET_OPTION(render_mode);
273 int next_mode = next_render_mode(current_mode);
274 options_set_int("render_mode", next_mode);
275
276 const char *mode_names[] = {"Foreground", "Background", "Half-block"};
277 if (next_mode >= 0 && next_mode <= 2) {
278 log_info("Render mode: %s", mode_names[next_mode]);
279 }
280 break;
281 }
282
283 // ===== COLOR FILTER CONTROL =====
284 case KEY_F:
285 case 'F': {
286 int current_filter = (int)GET_OPTION(color_filter);
287 int next_filter = next_color_filter(current_filter);
288 options_set_int("color_filter", next_filter);
289
290 if (next_filter >= 0 && next_filter < (int)COLOR_FILTER_COUNT) {
291 log_info("Color filter: %s", COLOR_FILTER_NAMES[next_filter]);
292 }
293 break;
294 }
295
296 // ===== HORIZONTAL FLIP CONTROL =====
297 case 'X':
298 case 'x': {
299 log_info("✓ X KEY PRESSED - handling horizontal flip");
300 bool current_flip_x = (bool)GET_OPTION(flip_x);
301 options_set_bool("flip_x", !current_flip_x);
302 log_info("Horizontal flip: %s (was: %s)", !current_flip_x ? "enabled" : "disabled",
303 current_flip_x ? "enabled" : "disabled");
304 break;
305 }
306
307 // ===== VERTICAL FLIP CONTROL =====
308 case 'Y':
309 case 'y': {
310 bool current_flip_y = (bool)GET_OPTION(flip_y);
311 options_set_bool("flip_y", !current_flip_y);
312 log_info("Vertical flip: %s", !current_flip_y ? "enabled" : "disabled");
313 break;
314 }
315
316 // ===== MATRIX RAIN EFFECT CONTROL =====
317 case KEY_0: {
318 bool current_matrix = (bool)GET_OPTION(matrix_rain);
319 options_set_bool("matrix_rain", !current_matrix);
320 log_info("Matrix rain effect: %s", !current_matrix ? "enabled" : "disabled");
321 break;
322 }
323
324 // ===== FPS COUNTER TOGGLE =====
325 case KEY_MINUS: {
326 bool current = (bool)GET_OPTION(fps_counter);
327 options_set_bool("fps_counter", !current);
328 log_info("FPS counter: %s", !current ? "enabled" : "disabled");
329 break;
330 }
331
332 // ===== LOCK DEBUG (debug builds only) =====
333#ifndef NDEBUG
334 case KEY_BACKTICK: {
336 log_debug("Lock state dump triggered via backtick key");
337 break;
338 }
339#endif
340
341 default:
342 // Unknown key - log it for debugging
343 if (key != KEY_NONE) {
344 log_info("UNKNOWN KEY: code=%d (0x%02x) char='%c'", key, key, (key >= 32 && key < 127) ? key : '?');
345 }
346 break;
347 }
348
349 // Re-render help screen if it's active to show updated settings
350 if (display && keyboard_help_is_active(display)) {
351 keyboard_help_render(display);
352 }
353}
void * session_capture_get_audio_context(session_capture_ctx_t *ctx)
Get the audio context for playback control.
void audio_flush_playback_buffers(audio_context_t *ctx)
Flush all audio buffers to synchronize with seek operations.
void debug_sync_trigger_print(void)
Trigger sync state print immediately (synchronous)
Definition sync.c:710
bool media_source_is_paused(media_source_t *source)
Check if media source is paused.
Definition source.c:1022
double media_source_get_position(media_source_t *source)
Get current playback position in seconds.
Definition source.c:956
double media_source_get_duration(media_source_t *source)
Get media duration in seconds.
Definition source.c:934
void media_source_toggle_pause(media_source_t *source)
Toggle pause state of media source.
Definition source.c:1032
asciichat_error_t options_set_int(const char *field_name, int value)
Check if an option was explicitly set via command-line.
Definition rcu.c:761
asciichat_error_t options_set_double(const char *field_name, double value)
Definition rcu.c:1283
asciichat_error_t options_set_bool(const char *field_name, bool value)
Definition rcu.c:969
@ KEY_SPACE
Space bar.
Definition keyboard.h:57
@ KEY_UP
Up arrow key.
Definition keyboard.h:58
@ KEY_M
'm' key - toggle mute
Definition keyboard.h:70
@ KEY_BACKTICK
'‘’ key - print lock state (debug builds)
Definition keyboard.h:73
@ KEY_ESCAPE
Escape key (ESC)
Definition keyboard.h:56
@ KEY_F
'f' key - flip webcam
Definition keyboard.h:71
@ KEY_MINUS
'-' key - toggle FPS counter
Definition keyboard.h:66
@ KEY_LEFT
Left arrow key.
Definition keyboard.h:60
@ KEY_RIGHT
Right arrow key.
Definition keyboard.h:61
@ KEY_0
'0' key - toggle matrix rain effect
Definition keyboard.h:67
@ KEY_NONE
No key pressed or no input available.
Definition keyboard.h:55
@ KEY_C
'c' key - cycle color modes
Definition keyboard.h:68
@ KEY_R
'r' key - cycle render modes
Definition keyboard.h:69
@ KEY_DOWN
Down arrow key.
Definition keyboard.h:59
@ KEY_QUESTION
'?' key - show help screen
Definition keyboard.h:72
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 centered on terminal.
void * session_capture_get_media_source(session_capture_ctx_t *ctx)
Get the underlying media source from capture context.
Media source for video and audio capture.
Definition source.c:34
@ COLOR_FILTER_COUNT
Total count of filters (not a valid filter)
Definition terminal.h:627

References ASCIICHAT_OK, audio_flush_playback_buffers(), bool, COLOR_FILTER_COUNT, debug_sync_trigger_print(), GET_OPTION, KEY_0, KEY_BACKTICK, KEY_C, KEY_DOWN, KEY_ESCAPE, KEY_F, KEY_LEFT, KEY_M, KEY_MINUS, KEY_NONE, KEY_QUESTION, KEY_R, KEY_RIGHT, KEY_SPACE, KEY_UP, keyboard_help_is_active(), keyboard_help_render(), keyboard_help_toggle(), log_debug, log_error, log_info, MEDIA_SOURCE_FILE, media_source_get_duration(), media_source_get_position(), media_source_get_type(), media_source_is_paused(), media_source_seek(), media_source_toggle_pause(), options_set_bool(), options_set_double(), options_set_int(), session_capture_get_audio_context(), session_capture_get_media_source(), and terminal_clear_screen().

Referenced by display_render_frame().

◆ session_host_add_client()

uint32_t session_host_add_client ( session_host_t *  host,
socket_t  socket,
const char *  ip,
int  port 
)

#include <host.h>

Add a client from an accepted socket.

Parameters
hostHost handle (must not be NULL)
socketAccepted client socket
ipClient IP address
portClient port
Returns
Assigned client ID on success, 0 on failure

Registers a new client from an accepted socket connection. This is typically called internally by the accept loop, but can be used for testing.

Note
on_client_join callback is invoked on successful registration.

Definition at line 1157 of file host.c.

1157 {
1158 if (!host || !host->initialized) {
1159 return 0;
1160 }
1161
1162 mutex_lock(&host->clients_mutex);
1163
1164 // Check if we have room
1165 if (host->client_count >= host->max_clients) {
1167 SET_ERRNO(ERROR_SESSION_FULL, "Maximum clients reached");
1168 return 0;
1169 }
1170
1171 // Find empty slot
1172 for (int i = 0; i < host->max_clients; i++) {
1173 if (!host->clients[i].active) {
1175 host->clients[i].client_id = host->next_client_id++;
1176 host->clients[i].socket = socket;
1177 if (ip) {
1178 SAFE_STRNCPY(host->clients[i].ip_address, ip, sizeof(host->clients[i].ip_address));
1179 }
1180 host->clients[i].port = port;
1181 host->clients[i].active = true;
1182 host->clients[i].video_active = false;
1183 host->clients[i].audio_active = false;
1184 host->clients[i].connected_at = (uint64_t)time(NULL);
1185 host->clients[i].transport = NULL; // No alternative transport initially
1186 host->clients[i].render_width = 0;
1187 host->clients[i].render_height = 0;
1188
1189 // Allocate media buffers
1190 host->clients[i].incoming_video = image_new(480, 270); // Network-optimal size (HD preview)
1191 host->clients[i].incoming_audio = ringbuffer_create(sizeof(float), 960 * 10); // ~200ms buffer @ 48kHz
1192
1193 if (!host->clients[i].incoming_video || !host->clients[i].incoming_audio) {
1194 // Cleanup on allocation failure
1195 if (host->clients[i].incoming_video) {
1197 host->clients[i].incoming_video = NULL;
1198 }
1199 if (host->clients[i].incoming_audio) {
1201 host->clients[i].incoming_audio = NULL;
1202 }
1203 host->clients[i].active = false;
1205 SET_ERRNO(ERROR_MEMORY, "Failed to allocate media buffers for client");
1206 return 0;
1207 }
1208
1209 uint32_t client_id = host->clients[i].client_id;
1210 host->client_count++;
1211
1213
1214 // Invoke callback
1215 if (host->callbacks.on_client_join) {
1216 host->callbacks.on_client_join(host, client_id, host->user_data);
1217 }
1218
1219 return client_id;
1220 }
1221 }
1222
1224 return 0;
1225}
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
@ ERROR_SESSION_FULL
Definition error_codes.h:87
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
ringbuffer_t * ringbuffer_create(size_t element_size, size_t capacity)
Create a new ring buffer.
Definition ringbuffer.c:29
void ringbuffer_destroy(ringbuffer_t *rb)
Destroy a ring buffer and free its memory.
Definition ringbuffer.c:53
void(* on_client_join)(session_host_t *h, uint32_t client_id, void *user_data)
Called when a client joins the session.
Definition host.h:111
ringbuffer_t * incoming_audio
Incoming audio ringbuffer (written by receive loop, read by render thread)
Definition host.c:83
char ip_address[64]
Definition host.c:67
socket_t socket
Definition host.c:66
uint16_t render_height
Definition host.c:74
uint32_t client_id
Definition host.c:65
participant_type_t participant_type
Definition host.c:64
uint64_t connected_at
Definition host.c:72
image_t * incoming_video
Incoming video frame buffer (for host render thread)
Definition host.c:80
uint16_t render_width
Definition host.c:73
struct acip_transport * transport
Alternative transport (WebRTC, WebSocket, etc.) - NULL if using socket only.
Definition host.c:77
mutex_t clients_mutex
Client list mutex.
Definition host.c:156
bool initialized
Context is initialized.
Definition host.c:186
int client_count
Current client count.
Definition host.c:150
session_host_client_t * clients
Client array.
Definition host.c:147
void * user_data
User data for callbacks.
Definition host.c:132
session_host_callbacks_t callbacks
Event callbacks.
Definition host.c:129
int max_clients
Maximum clients.
Definition host.c:117
uint32_t next_client_id
Next client ID counter.
Definition host.c:153

References session_host_client_t::active, session_host_client_t::audio_active, session_host::callbacks, session_host::client_count, session_host_client_t::client_id, session_host::clients, session_host::clients_mutex, session_host_client_t::connected_at, ERROR_MEMORY, ERROR_SESSION_FULL, image_destroy(), image_new(), session_host_client_t::incoming_audio, session_host_client_t::incoming_video, session_host::initialized, session_host_client_t::ip_address, session_host::max_clients, mutex_lock, mutex_unlock, session_host::next_client_id, session_host_callbacks_t::on_client_join, session_host_client_t::participant_type, PARTICIPANT_TYPE_NETWORK, session_host_client_t::port, session_host_client_t::render_height, session_host_client_t::render_width, ringbuffer_create(), ringbuffer_destroy(), SAFE_STRNCPY, SET_ERRNO, session_host_client_t::socket, session_host_client_t::transport, session_host::user_data, and session_host_client_t::video_active.

Referenced by add_client(), and add_webrtc_client().

◆ session_host_add_memory_participant()

uint32_t session_host_add_memory_participant ( session_host_t *  host)

#include <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 1227 of file host.c.

1227 {
1228 if (!host || !host->initialized) {
1229 return 0;
1230 }
1231
1232 mutex_lock(&host->clients_mutex);
1233
1234 // Check if we have room
1235 if (host->client_count >= host->max_clients) {
1237 SET_ERRNO(ERROR_SESSION_FULL, "Maximum clients reached");
1238 return 0;
1239 }
1240
1241 // Check if memory participant already exists (only one allowed)
1242 for (int i = 0; i < host->max_clients; i++) {
1243 if (host->clients[i].active && host->clients[i].participant_type == PARTICIPANT_TYPE_MEMORY) {
1245 SET_ERRNO(ERROR_INVALID_PARAM, "Memory participant already exists");
1246 return 0;
1247 }
1248 }
1249
1250 // Find empty slot
1251 for (int i = 0; i < host->max_clients; i++) {
1252 if (!host->clients[i].active) {
1254 host->clients[i].client_id = host->next_client_id++;
1255 host->clients[i].socket = INVALID_SOCKET_VALUE; // No socket for memory participant
1256 SAFE_STRNCPY(host->clients[i].ip_address, "memory", sizeof(host->clients[i].ip_address));
1257 host->clients[i].port = 0;
1258 host->clients[i].active = true;
1259 host->clients[i].video_active = true; // Memory participants always have active video/audio
1260 host->clients[i].audio_active = true;
1261 host->clients[i].connected_at = (uint64_t)time(NULL);
1262 host->clients[i].transport = NULL;
1263
1264 // Allocate media buffers (same as network clients)
1265 host->clients[i].incoming_video = image_new(480, 270);
1266 host->clients[i].incoming_audio = ringbuffer_create(sizeof(float), 960 * 10);
1267
1268 if (!host->clients[i].incoming_video || !host->clients[i].incoming_audio) {
1269 if (host->clients[i].incoming_video) {
1271 host->clients[i].incoming_video = NULL;
1272 }
1273 if (host->clients[i].incoming_audio) {
1275 host->clients[i].incoming_audio = NULL;
1276 }
1277 host->clients[i].active = false;
1279 SET_ERRNO(ERROR_MEMORY, "Failed to allocate media buffers for memory participant");
1280 return 0;
1281 }
1282
1284 host->client_count++;
1285
1287
1288 log_info("Added memory participant with ID %u", participant_id);
1289
1290 // Invoke callback
1291 if (host->callbacks.on_client_join) {
1293 }
1294
1295 return participant_id;
1296 }
1297 }
1298
1300 return 0;
1301}
#define INVALID_SOCKET_VALUE
Invalid socket value (POSIX: -1)
Definition socket.h:278
uint8_t participant_id[16]

References session_host_client_t::active, session_host_client_t::audio_active, session_host::callbacks, session_host::client_count, session_host_client_t::client_id, session_host::clients, session_host::clients_mutex, session_host_client_t::connected_at, ERROR_INVALID_PARAM, ERROR_MEMORY, ERROR_SESSION_FULL, image_destroy(), image_new(), session_host_client_t::incoming_audio, session_host_client_t::incoming_video, session_host::initialized, INVALID_SOCKET_VALUE, session_host_client_t::ip_address, log_info, session_host::max_clients, mutex_lock, mutex_unlock, session_host::next_client_id, session_host_callbacks_t::on_client_join, participant_id, session_host_client_t::participant_type, PARTICIPANT_TYPE_MEMORY, session_host_client_t::port, ringbuffer_create(), ringbuffer_destroy(), SAFE_STRNCPY, SET_ERRNO, session_host_client_t::socket, session_host_client_t::transport, session_host::user_data, and session_host_client_t::video_active.

◆ session_host_broadcast_frame()

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

#include <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, "session_host_broadcast_frame: invalid parameter");
1492 }
1493
1494 if (!host->running) {
1495 return SET_ERRNO(ERROR_INVALID_STATE, "session_host_broadcast_frame: not running");
1496 }
1497
1498 // Broadcast ASCII frame to all connected clients
1499 size_t frame_len = strlen(frame) + 1; // Include null terminator
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 =
1506 acip_send_ascii_frame(host->clients[i].transport, frame, frame_len - 1, 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 } else if (host->clients[i].active && host->clients[i].socket != INVALID_SOCKET_VALUE) {
1512 asciichat_error_t send_result = packet_send(host->clients[i].socket, PACKET_TYPE_ASCII_FRAME, frame, frame_len);
1513 if (send_result != ASCIICHAT_OK) {
1514 log_warn("Failed to send ASCII frame to client %u", host->clients[i].client_id);
1515 result = send_result; // Store error but continue broadcasting to other clients
1516 }
1517 }
1518 }
1520
1521 return result;
1522}
asciichat_error_t packet_send(socket_t sockfd, packet_type_t type, const void *data, size_t len)
Send a packet with proper header and CRC32.
Definition packet.c:292
@ PACKET_TYPE_ASCII_FRAME
Complete ASCII frame with all metadata.
Definition packet.h:361
asciichat_error_t acip_send_ascii_frame(acip_transport_t *transport, const char *frame_data, size_t frame_size, uint32_t width, uint32_t height, const char *client_id)
Send ASCII frame to client (server → client)
bool running
Server is running.
Definition host.c:144

References acip_send_ascii_frame(), session_host_client_t::active, ASCIICHAT_OK, session_host_client_t::client_id, session_host::clients, session_host::clients_mutex, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, frame, session_host::initialized, INVALID_SOCKET_VALUE, log_warn, session_host::max_clients, mutex_lock, mutex_unlock, packet_send(), PACKET_TYPE_ASCII_FRAME, session_host::running, SET_ERRNO, session_host_client_t::socket, and session_host_client_t::transport.

◆ session_host_client_has_transport()

bool session_host_client_has_transport ( session_host_t *  host,
uint32_t  client_id 
)

#include <host.h>

Check if a specific client has an active alternative transport.

Convenient way to check if a client is using an alternative transport (WebRTC, WebSocket, etc.) instead of raw TCP socket.

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

Definition at line 1738 of file host.c.

1738 {
1739 if (!host || !host->initialized) {
1740 return false;
1741 }
1742
1743 mutex_lock(&host->clients_mutex);
1744
1745 // Find client with matching ID
1746 for (int i = 0; i < host->max_clients; i++) {
1747 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1748 bool has_transport = host->clients[i].transport != NULL;
1750 return has_transport;
1751 }
1752 }
1753
1755 return false;
1756}

References session_host_client_t::active, session_host_client_t::client_id, session_host::clients, session_host::clients_mutex, session_host::initialized, session_host::max_clients, mutex_lock, mutex_unlock, and session_host_client_t::transport.

◆ session_host_create()

session_host_t * session_host_create ( const session_host_config_t *  config)

#include <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 193 of file host.c.

193 {
194 if (!config) {
195 SET_ERRNO(ERROR_INVALID_PARAM, "session_host_create: NULL config");
196 return NULL;
197 }
198
199 // Allocate host
201
202 // Copy configuration
203 host->port = config->port > 0 ? config->port : OPT_PORT_INT_DEFAULT;
206
207 if (config->ipv4_address) {
208 SAFE_STRNCPY(host->ipv4_address, config->ipv4_address, sizeof(host->ipv4_address));
209 }
210
211 if (config->ipv6_address) {
212 SAFE_STRNCPY(host->ipv6_address, config->ipv6_address, sizeof(host->ipv6_address));
213 }
214
215 if (config->key_path) {
216 SAFE_STRNCPY(host->key_path, config->key_path, sizeof(host->key_path));
217 }
218
219 if (config->password) {
220 SAFE_STRNCPY(host->password, config->password, sizeof(host->password));
221 }
222
223 host->callbacks = config->callbacks;
224 host->user_data = config->user_data;
225
226 // Initialize sockets to invalid
229 host->running = false;
230
231 // Allocate client array
233
234 host->client_count = 0;
235 host->next_client_id = 1;
236
237 // Initialize mutex
238 if (mutex_init(&host->clients_mutex, "host_clients") != 0) {
239 SET_ERRNO(ERROR_THREAD, "Failed to initialize clients mutex");
240 SAFE_FREE(host->clients);
241 SAFE_FREE(host);
242 return NULL;
243 }
244
245 host->initialized = true;
246 return host;
247}
@ ERROR_THREAD
#define OPT_PORT_INT_DEFAULT
Default TCP port for client/server communication (integer)
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
#define SESSION_HOST_DEFAULT_MAX_CLIENTS
Default maximum clients.
Definition host.c:54
Internal client record structure.
Definition host.c:63
void * user_data
User-provided context pointer passed to callbacks.
Definition host.h:180
const char * ipv6_address
IPv6 address to bind to (NULL for any)
Definition host.h:162
session_host_callbacks_t callbacks
Event callbacks.
Definition host.h:177
const char * ipv4_address
IPv4 address to bind to (NULL for any)
Definition host.h:159
int max_clients
Maximum number of clients.
Definition host.h:165
char ipv6_address[64]
IPv6 bind address.
Definition host.c:114
char password[256]
Password.
Definition host.c:126
char ipv4_address[64]
IPv4 bind address.
Definition host.c:111
socket_t socket_v6
IPv6 listen socket.
Definition host.c:141
socket_t socket_v4
IPv4 listen socket.
Definition host.c:138
char key_path[512]
Key path.
Definition host.c:123
int port
Port to listen on.
Definition host.c:108
bool encryption_enabled
Encryption enabled.
Definition host.c:120

References session_host::callbacks, session_host_config_t::callbacks, session_host::client_count, session_host::clients, session_host::clients_mutex, session_host::encryption_enabled, session_host_config_t::encryption_enabled, ERROR_INVALID_PARAM, ERROR_THREAD, session_host::initialized, INVALID_SOCKET_VALUE, session_host::ipv4_address, session_host_config_t::ipv4_address, session_host::ipv6_address, session_host_config_t::ipv6_address, session_host::key_path, session_host_config_t::key_path, session_host::max_clients, session_host_config_t::max_clients, mutex_init(), session_host::next_client_id, OPT_PORT_INT_DEFAULT, session_host::password, session_host_config_t::password, session_host::port, session_host_config_t::port, session_host::running, SAFE_CALLOC, SAFE_FREE, SAFE_STRNCPY, SESSION_HOST_DEFAULT_MAX_CLIENTS, SET_ERRNO, session_host::socket_v4, session_host::socket_v6, session_host::user_data, and session_host_config_t::user_data.

Referenced by discovery_session_become_host(), and discovery_session_process().

◆ session_host_destroy()

void session_host_destroy ( session_host_t *  host)

#include <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 249 of file host.c.

249 {
250 if (!host) {
251 return;
252 }
253
254 // Stop if running
255 if (host->running) {
256 session_host_stop(host);
257 }
258
259 // Clean up audio resources
260 if (host->audio_ctx) {
262 host->audio_ctx = NULL;
263 }
264 if (host->opus_decoder) {
266 host->opus_decoder = NULL;
267 }
268
269 // Close sockets
270 if (host->socket_v4 != INVALID_SOCKET_VALUE) {
271 socket_close(host->socket_v4);
273 }
274 if (host->socket_v6 != INVALID_SOCKET_VALUE) {
275 socket_close(host->socket_v6);
277 }
278
279 // Free client array and per-client resources
280 if (host->clients) {
281 for (int i = 0; i < host->max_clients; i++) {
282 if (host->clients[i].incoming_video) {
284 host->clients[i].incoming_video = NULL;
285 }
286 if (host->clients[i].incoming_audio) {
288 host->clients[i].incoming_audio = NULL;
289 }
290 }
291 SAFE_FREE(host->clients);
292 }
293
294 // Destroy mutex
296
297 // Clear sensitive data
298 memset(host->password, 0, sizeof(host->password));
299
300 host->initialized = false;
301 SAFE_FREE(host);
302}
void opus_codec_destroy(opus_codec_t *codec)
Destroy an Opus codec instance.
Definition opus.c:230
int socket_close(socket_t sock)
Close a socket.
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
opus_codec_t * opus_decoder
Opus decoder for decoding incoming Opus audio.
Definition host.c:180
session_audio_ctx_t * audio_ctx
Audio context for mixing (host only)
Definition host.c:177

References session_host::audio_ctx, session_host::clients, session_host::clients_mutex, image_destroy(), session_host_client_t::incoming_audio, session_host_client_t::incoming_video, session_host::initialized, INVALID_SOCKET_VALUE, session_host::max_clients, mutex_destroy(), opus_codec_destroy(), session_host::opus_decoder, session_host::password, ringbuffer_destroy(), session_host::running, SAFE_FREE, session_audio_destroy(), session_host_stop(), socket_close(), session_host::socket_v4, and session_host::socket_v6.

Referenced by discovery_session_destroy().

◆ 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 <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, "session_host_find_client: 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
1452 return ASCIICHAT_OK;
1453 }
1454 }
1455
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:97
int port
Client port.
Definition host.h:88
char ip_address[64]
Client IP address.
Definition host.h:85
bool audio_active
Client is currently streaming audio.
Definition host.h:94
uint32_t client_id
Unique client identifier.
Definition host.h:82
bool video_active
Client is currently streaming video.
Definition host.h:91

References session_host_client_t::active, ASCIICHAT_OK, session_host_client_t::audio_active, session_host_client_info_t::audio_active, session_host_client_t::client_id, session_host_client_info_t::client_id, session_host::clients, session_host::clients_mutex, session_host_client_t::connected_at, session_host_client_info_t::connected_at, ERROR_INVALID_PARAM, ERROR_NOT_FOUND, session_host::initialized, session_host_client_t::ip_address, session_host_client_info_t::ip_address, session_host::max_clients, mutex_lock, mutex_unlock, session_host_client_t::port, session_host_client_info_t::port, SAFE_STRNCPY, SET_ERRNO, session_host_client_t::video_active, and session_host_client_info_t::video_active.

◆ session_host_get_client_count()

int session_host_get_client_count ( session_host_t *  host)

#include <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}

References session_host::client_count, and session_host::initialized.

◆ session_host_get_client_ids()

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

#include <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
1482 return count;
1483}

References session_host_client_t::active, session_host_client_t::client_id, session_host::clients, session_host::clients_mutex, session_host::initialized, session_host::max_clients, mutex_lock, and mutex_unlock.

◆ session_host_get_client_transport()

acip_transport_t * session_host_get_client_transport ( session_host_t *  host,
uint32_t  client_id 
)

#include <host.h>

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

Returns the currently active transport for the client, if any. May return NULL if only socket transport is in use.

Parameters
hostHost handle (must not be NULL)
client_idClient ID whose transport should be retrieved
Returns
Transport pointer if set, NULL if using socket transport or client not found

Definition at line 1718 of file host.c.

1718 {
1719 if (!host || !host->initialized) {
1720 return NULL;
1721 }
1722
1723 mutex_lock(&host->clients_mutex);
1724
1725 // Find client with matching ID
1726 for (int i = 0; i < host->max_clients; i++) {
1727 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1728 acip_transport_t *transport = host->clients[i].transport;
1730 return transport;
1731 }
1732 }
1733
1735 return NULL;
1736}
Transport instance structure.
Definition transport.h:214

References session_host_client_t::active, session_host_client_t::client_id, session_host::clients, session_host::clients_mutex, session_host::initialized, session_host::max_clients, mutex_lock, mutex_unlock, and session_host_client_t::transport.

◆ 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 <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 1348 of file host.c.

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

References session_host_client_t::active, ASCIICHAT_OK, session_host_client_t::audio_active, session_host_client_t::client_id, session_host::clients, session_host::clients_mutex, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_NOT_FOUND, session_host_client_t::incoming_audio, session_host::initialized, log_warn_every, session_host::max_clients, mutex_lock, mutex_unlock, NS_PER_MS_INT, participant_id, session_host_client_t::participant_type, 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 <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 1303 of file host.c.

1303 {
1304 if (!host || !host->initialized || !frame) {
1305 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_inject_frame: invalid parameters");
1306 }
1307
1308 mutex_lock(&host->clients_mutex);
1309
1310 // Find memory participant
1311 for (int i = 0; i < host->max_clients; i++) {
1312 if (host->clients[i].active && host->clients[i].client_id == participant_id &&
1314
1315 if (!host->clients[i].incoming_video) {
1317 return SET_ERRNO(ERROR_INVALID_STATE, "Memory participant has no video buffer");
1318 }
1319
1320 // Copy frame data into buffer
1321 image_t *dest = host->clients[i].incoming_video;
1322 if (dest->w != frame->w || dest->h != frame->h) {
1323 // Reallocate if size changed
1324 image_destroy(dest);
1325 dest = image_new(frame->w, frame->h);
1326 if (!dest) {
1327 host->clients[i].incoming_video = NULL;
1329 return SET_ERRNO(ERROR_MEMORY, "Failed to reallocate video buffer");
1330 }
1331 host->clients[i].incoming_video = dest;
1332 }
1333
1334 // Copy pixel data
1335 memcpy(dest->pixels, frame->pixels, frame->w * frame->h * sizeof(rgb_pixel_t));
1336
1337 host->clients[i].video_active = true;
1338
1340 return ASCIICHAT_OK;
1341 }
1342 }
1343
1345 return SET_ERRNO(ERROR_NOT_FOUND, "Memory participant not found");
1346}

References session_host_client_t::active, ASCIICHAT_OK, session_host_client_t::client_id, session_host::clients, session_host::clients_mutex, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_MEMORY, ERROR_NOT_FOUND, frame, image_t::h, image_destroy(), image_new(), session_host_client_t::incoming_video, session_host::initialized, session_host::max_clients, mutex_lock, mutex_unlock, participant_id, session_host_client_t::participant_type, PARTICIPANT_TYPE_MEMORY, image_t::pixels, SET_ERRNO, session_host_client_t::video_active, and image_t::w.

◆ session_host_is_running()

bool session_host_is_running ( session_host_t *  host)

#include <host.h>

Check if host is running.

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

Definition at line 1146 of file host.c.

1146 {
1147 if (!host || !host->initialized) {
1148 return false;
1149 }
1150 return host->running;
1151}

References session_host::initialized, and session_host::running.

◆ session_host_remove_client()

asciichat_error_t session_host_remove_client ( session_host_t *  host,
uint32_t  client_id 
)

#include <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 1391 of file host.c.

1391 {
1392 if (!host || !host->initialized) {
1393 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_remove_client: invalid host");
1394 }
1395
1396 mutex_lock(&host->clients_mutex);
1397
1398 for (int i = 0; i < host->max_clients; i++) {
1399 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1400 // Invoke callback before removing
1402 if (host->callbacks.on_client_leave) {
1403 host->callbacks.on_client_leave(host, client_id, host->user_data);
1404 }
1405 mutex_lock(&host->clients_mutex);
1406
1407 // Close socket
1408 if (host->clients[i].socket != INVALID_SOCKET_VALUE) {
1409 socket_close(host->clients[i].socket);
1411 }
1412
1413 // Clean up media buffers
1414 if (host->clients[i].incoming_video) {
1416 host->clients[i].incoming_video = NULL;
1417 }
1418 if (host->clients[i].incoming_audio) {
1420 host->clients[i].incoming_audio = NULL;
1421 }
1422
1423 host->clients[i].active = false;
1424 host->client_count--;
1425
1427 return ASCIICHAT_OK;
1428 }
1429 }
1430
1432 return SET_ERRNO(ERROR_NOT_FOUND, "Client not found: %u", client_id);
1433}
void(* on_client_leave)(session_host_t *h, uint32_t client_id, void *user_data)
Called when a client leaves the session.
Definition host.h:119

References session_host_client_t::active, ASCIICHAT_OK, session_host::callbacks, session_host::client_count, session_host_client_t::client_id, session_host::clients, session_host::clients_mutex, ERROR_INVALID_PARAM, ERROR_NOT_FOUND, image_destroy(), session_host_client_t::incoming_audio, session_host_client_t::incoming_video, session_host::initialized, INVALID_SOCKET_VALUE, session_host::max_clients, mutex_lock, mutex_unlock, session_host_callbacks_t::on_client_leave, ringbuffer_destroy(), SET_ERRNO, session_host_client_t::socket, socket_close(), and session_host::user_data.

Referenced by remove_client().

◆ session_host_send_frame()

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

#include <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 1524 of file host.c.

1524 {
1525 if (!host || !host->initialized || !frame) {
1526 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_send_frame: invalid parameter");
1527 }
1528
1529 if (!host->running) {
1530 return SET_ERRNO(ERROR_INVALID_STATE, "session_host_send_frame: not running");
1531 }
1532
1533 // Send ASCII frame to specific client
1534 size_t frame_len = strlen(frame) + 1; // Include null terminator
1535
1536 mutex_lock(&host->clients_mutex);
1537 for (int i = 0; i < host->max_clients; i++) {
1538 if (host->clients[i].client_id == client_id && host->clients[i].active) {
1539 asciichat_error_t result =
1540 host->clients[i].transport
1541 ? acip_send_ascii_frame(host->clients[i].transport, frame, frame_len - 1, 80, 24, "session-host")
1542 : (host->clients[i].socket != INVALID_SOCKET_VALUE
1543 ? packet_send(host->clients[i].socket, PACKET_TYPE_ASCII_FRAME, frame, frame_len)
1544 : ERROR_NOT_FOUND);
1546 return result;
1547 }
1548 }
1550
1551 return SET_ERRNO(ERROR_NOT_FOUND, "session_host_send_frame: client %u not found", client_id);
1552}

References acip_send_ascii_frame(), session_host_client_t::active, session_host_client_t::client_id, session_host::clients, session_host::clients_mutex, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_NOT_FOUND, frame, session_host::initialized, INVALID_SOCKET_VALUE, session_host::max_clients, mutex_lock, mutex_unlock, packet_send(), PACKET_TYPE_ASCII_FRAME, session_host::running, SET_ERRNO, session_host_client_t::socket, and session_host_client_t::transport.

◆ 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 
)

#include <host.h>

Set an alternative transport for a specific client in the host.

Allows replacing a client's default TCP socket transport with an alternative transport such as WebRTC DataChannel. Once set, the host will use this transport for send/receive operations with the specified client.

Parameters
hostHost handle (must not be NULL)
client_idClient ID whose transport should be replaced
transportAlternative transport to use (can be NULL to clear)
Returns
ASCIICHAT_OK on success, ERROR_NOT_FOUND if client not found, other error on failure
Note
This is used for WebRTC integration when DataChannel becomes ready
Transport ownership remains with caller (host does not free it)
If both socket and transport exist, transport takes precedence

Definition at line 1666 of file host.c.

1667 {
1668 if (!host || !host->initialized) {
1669 return SET_ERRNO(ERROR_INVALID_PARAM, "Host is NULL or not initialized");
1670 }
1671
1672 mutex_lock(&host->clients_mutex);
1673
1674 // Find client with matching ID
1675 for (int i = 0; i < host->max_clients; i++) {
1676 if (host->clients[i].active && host->clients[i].client_id == client_id) {
1677 log_info("session_host_set_client_transport: Setting transport=%p for client %u (was=%p)", transport, client_id,
1678 host->clients[i].transport);
1679
1680 host->clients[i].transport = transport;
1681
1682 if (transport) {
1683 log_info("WebRTC transport now active for client %u", client_id);
1684 } else {
1685 log_info("WebRTC transport cleared for client %u, reverting to socket", client_id);
1686 }
1687
1688 server_state_packet_t state = {0};
1689 if (transport) {
1690 uint32_t active_count = 0;
1691 for (int j = 0; j < host->max_clients; j++) {
1692 if (host->clients[j].active) {
1693 active_count++;
1694 }
1695 }
1696 state.connected_client_count = HOST_TO_NET_U32(active_count);
1697 state.active_client_count = HOST_TO_NET_U32(active_count);
1698 }
1700
1701 if (transport) {
1702 // Discovery sessions bypass the native server handshake. Notify the
1703 // browser after registering its transport so it can start media.
1704 asciichat_error_t send_result = acip_send_server_state(transport, &state);
1705 if (send_result != ASCIICHAT_OK) {
1706 return send_result;
1707 }
1708 }
1709 return ASCIICHAT_OK;
1710 }
1711 }
1712
1714 log_warn("Client %u not found", client_id);
1715 return SET_ERRNO(ERROR_NOT_FOUND, "Client not found");
1716}
#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:706
uint32_t active_client_count
Number of clients actively sending video/audio streams.
Definition packet.h:710
uint32_t connected_client_count
Total number of currently connected clients.
Definition packet.h:708

References acip_send_server_state(), session_host_client_t::active, server_state_packet_t::active_client_count, ASCIICHAT_OK, session_host_client_t::client_id, session_host::clients, session_host::clients_mutex, server_state_packet_t::connected_client_count, ERROR_INVALID_PARAM, ERROR_NOT_FOUND, HOST_TO_NET_U32, session_host::initialized, log_info, log_warn, session_host::max_clients, mutex_lock, mutex_unlock, SET_ERRNO, and session_host_client_t::transport.

◆ session_host_set_display()

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

#include <host.h>

Set 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 1558 of file host.c.

1558 {
1559 if (!host || !host->initialized) {
1560 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_set_display: invalid host");
1561 }
1562
1563 host->display_context = display;
1564 return ASCIICHAT_OK;
1565}
struct session_display_ctx * display_context
Display context for local frame rendering (optional, can be NULL)
Definition host.c:135

References ASCIICHAT_OK, session_host::display_context, ERROR_INVALID_PARAM, session_host::initialized, and SET_ERRNO.

◆ session_host_start()

asciichat_error_t session_host_start ( session_host_t *  host)

#include <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 1026 of file host.c.

1026 {
1027 if (!host || !host->initialized) {
1028 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_start: invalid host");
1029 }
1030
1031 if (host->running) {
1032 return ASCIICHAT_OK; // Already running
1033 }
1034
1035 // Create listen socket(s)
1036 // For simplicity, we bind to IPv4 if specified, otherwise default to 0.0.0.0
1037 const char *bind_address = host->ipv4_address[0] ? host->ipv4_address : "0.0.0.0";
1038
1039 host->socket_v4 = create_listen_socket(bind_address, host->port);
1040 if (host->socket_v4 == INVALID_SOCKET_VALUE) {
1041 log_error("Failed to create IPv4 listen socket");
1042 if (host->callbacks.on_error) {
1043 host->callbacks.on_error(host, ERROR_NETWORK_BIND, "Failed to create listen socket", host->user_data);
1044 }
1045 return GET_ERRNO();
1046 }
1047
1048 host->running = true;
1049 log_info("Session host listening on %s:%d", bind_address, host->port);
1050
1051 // Spawn accept loop thread
1052 host->accept_thread_running = true;
1053 if (asciichat_thread_create(&host->accept_thread, "accept", accept_loop_thread, host) != 0) {
1054 log_error("Failed to spawn accept loop thread");
1055 host->accept_thread_running = false;
1056 if (host->callbacks.on_error) {
1057 host->callbacks.on_error(host, ERROR_THREAD, "Failed to spawn accept loop thread", host->user_data);
1058 }
1059 socket_close(host->socket_v4);
1061 host->running = false;
1062 return SET_ERRNO(ERROR_THREAD, "Failed to spawn accept loop thread");
1063 }
1064
1065 // Spawn receive loop thread
1066 host->receive_thread_running = true;
1067 if (asciichat_thread_create(&host->receive_thread, "host_recv", receive_loop_thread, host) != 0) {
1068 log_error("Failed to spawn receive loop thread");
1069 host->receive_thread_running = false;
1070 host->accept_thread_running = false;
1072 if (host->callbacks.on_error) {
1073 host->callbacks.on_error(host, ERROR_THREAD, "Failed to spawn receive loop thread", host->user_data);
1074 }
1075 socket_close(host->socket_v4);
1077 host->running = false;
1078 return SET_ERRNO(ERROR_THREAD, "Failed to spawn receive loop thread");
1079 }
1080
1081 // Spawn render thread (optional - can be started later)
1082 // This thread handles video mixing and audio distribution
1083 return ASCIICHAT_OK;
1084}
@ ERROR_NETWORK_BIND
Definition error_codes.h:78
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)
#define asciichat_thread_join(thread, timeout_ms)
void(* on_error)(session_host_t *h, asciichat_error_t error, const char *message, void *user_data)
Called when an error occurs.
Definition host.h:147
asciichat_thread_t receive_thread
Receive thread handle.
Definition host.c:165
bool receive_thread_running
Receive thread is running.
Definition host.c:168
asciichat_thread_t accept_thread
Accept thread handle.
Definition host.c:159
bool accept_thread_running
Accept thread is running.
Definition host.c:162

References session_host::accept_thread, session_host::accept_thread_running, ASCIICHAT_OK, asciichat_thread_create, asciichat_thread_join, session_host::callbacks, ERROR_INVALID_PARAM, ERROR_NETWORK_BIND, ERROR_THREAD, GET_ERRNO, session_host::initialized, INVALID_SOCKET_VALUE, session_host::ipv4_address, log_error, log_info, session_host_callbacks_t::on_error, session_host::port, session_host::receive_thread, session_host::receive_thread_running, session_host::running, SET_ERRNO, socket_close(), session_host::socket_v4, and session_host::user_data.

Referenced by discovery_session_become_host(), and discovery_session_process().

◆ session_host_start_render()

asciichat_error_t session_host_start_render ( session_host_t *  host)

#include <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 1571 of file host.c.

1571 {
1572 if (!host || !host->initialized) {
1573 return SET_ERRNO(ERROR_INVALID_PARAM, "session_host_start_render: invalid host");
1574 }
1575
1576 // Discovery mode starts the shared host renderer without passing through
1577 // server/main.c, which normally initializes the ASCII lookup palettes.
1579
1580 if (!host->running) {
1581 return SET_ERRNO(ERROR_INVALID_STATE, "session_host_start_render: not running");
1582 }
1583
1584 if (host->render_thread_running) {
1585 return ASCIICHAT_OK; // Already running
1586 }
1587
1588 // Create audio context for mixing (host mode = true)
1589 if (!host->audio_ctx) {
1590 host->audio_ctx = session_audio_create(true);
1591 if (!host->audio_ctx) {
1592 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to create audio context");
1593 }
1594 }
1595
1596 // Create Opus decoder for decoding incoming Opus audio (48kHz)
1597 if (!host->opus_decoder) {
1599 if (!host->opus_decoder) {
1601 host->audio_ctx = NULL;
1602 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to create Opus decoder");
1603 }
1604 }
1605
1606 // Create Opus encoder for encoding mixed audio for broadcast (48kHz, VOIP mode, 24kbps)
1607 if (!host->opus_encoder) {
1609 if (!host->opus_encoder) {
1611 host->opus_decoder = NULL;
1613 host->audio_ctx = NULL;
1614 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to create Opus encoder");
1615 }
1616 }
1617
1618 // Spawn render thread
1619 host->render_thread_running = true;
1620 if (asciichat_thread_create(&host->render_thread, "render", host_render_thread, host) != 0) {
1621 log_error("Failed to spawn render thread");
1622 host->render_thread_running = false;
1623 return SET_ERRNO(ERROR_THREAD, "Failed to spawn render thread");
1624 }
1625
1626 log_info("Host render thread started");
1627 return ASCIICHAT_OK;
1628}
opus_codec_t * opus_codec_create_decoder(int sample_rate)
Create an Opus decoder.
Definition opus.c:74
opus_codec_t * opus_codec_create_encoder(opus_application_t application, int sample_rate, int bitrate)
Create an Opus encoder.
Definition opus.c:19
@ OPUS_APPLICATION_VOIP
Voice over IP (optimized for speech)
Definition opus.h:77
void ascii_simd_init(void)
Initialize SIMD subsystem.
opus_codec_t * opus_encoder
Opus encoder for encoding mixed audio for broadcast.
Definition host.c:183
asciichat_thread_t render_thread
Render thread handle (for video mixing and audio distribution)
Definition host.c:171
bool render_thread_running
Render thread is running.
Definition host.c:174

References ascii_simd_init(), ASCIICHAT_OK, asciichat_thread_create, session_host::audio_ctx, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_THREAD, session_host::initialized, log_error, log_info, OPUS_APPLICATION_VOIP, opus_codec_create_decoder(), opus_codec_create_encoder(), opus_codec_destroy(), session_host::opus_decoder, session_host::opus_encoder, session_host::render_thread, session_host::render_thread_running, session_host::running, session_audio_create(), session_audio_destroy(), and SET_ERRNO.

◆ session_host_stop()

void session_host_stop ( session_host_t *  host)

#include <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 1086 of file host.c.

1086 {
1087 if (!host || !host->initialized || !host->running) {
1088 return;
1089 }
1090
1091 // Stop render thread if running
1092 if (host->render_thread_running) {
1093 host->render_thread_running = false;
1095 log_info("Render thread joined");
1096 }
1097
1098 // Stop receive loop thread (reads from client sockets)
1099 if (host->receive_thread_running) {
1100 host->receive_thread_running = false;
1102 log_info("Receive loop thread joined");
1103 }
1104
1105 // Stop accept loop thread (before closing listen socket)
1106 if (host->accept_thread_running) {
1107 host->accept_thread_running = false;
1109 log_info("Accept loop thread joined");
1110 }
1111
1112 // Disconnect all clients
1113 mutex_lock(&host->clients_mutex);
1114 for (int i = 0; i < host->max_clients; i++) {
1115 if (host->clients[i].active) {
1116 // Invoke callback
1117 if (host->callbacks.on_client_leave) {
1118 host->callbacks.on_client_leave(host, host->clients[i].client_id, host->user_data);
1119 }
1120
1121 // Close socket
1122 if (host->clients[i].socket != INVALID_SOCKET_VALUE) {
1123 socket_close(host->clients[i].socket);
1125 }
1126
1127 host->clients[i].active = false;
1128 }
1129 }
1130 host->client_count = 0;
1132
1133 // Close listen sockets
1134 if (host->socket_v4 != INVALID_SOCKET_VALUE) {
1135 socket_close(host->socket_v4);
1137 }
1138 if (host->socket_v6 != INVALID_SOCKET_VALUE) {
1139 socket_close(host->socket_v6);
1141 }
1142
1143 host->running = false;
1144}

References session_host::accept_thread, session_host::accept_thread_running, session_host_client_t::active, asciichat_thread_join, session_host::callbacks, session_host::client_count, session_host_client_t::client_id, session_host::clients, session_host::clients_mutex, session_host::initialized, INVALID_SOCKET_VALUE, log_info, session_host::max_clients, mutex_lock, mutex_unlock, session_host_callbacks_t::on_client_leave, session_host::receive_thread, session_host::receive_thread_running, session_host::render_thread, session_host::render_thread_running, session_host::running, session_host_client_t::socket, socket_close(), session_host::socket_v4, session_host::socket_v6, and session_host::user_data.

Referenced by session_host_destroy().

◆ session_host_stop_render()

void session_host_stop_render ( session_host_t *  host)

#include <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 1630 of file host.c.

1630 {
1631 if (!host || !host->initialized) {
1632 return;
1633 }
1634
1635 if (!host->render_thread_running) {
1636 return;
1637 }
1638
1639 // Signal thread to stop
1640 host->render_thread_running = false;
1641
1642 // Wait for thread to complete
1644
1645 // Clean up audio resources
1646 if (host->audio_ctx) {
1648 host->audio_ctx = NULL;
1649 }
1650 if (host->opus_decoder) {
1652 host->opus_decoder = NULL;
1653 }
1654 if (host->opus_encoder) {
1656 host->opus_encoder = NULL;
1657 }
1658
1659 log_info("Host render thread stopped");
1660}

References asciichat_thread_join, session_host::audio_ctx, session_host::initialized, log_info, opus_codec_destroy(), session_host::opus_decoder, session_host::opus_encoder, session_host::render_thread, session_host::render_thread_running, and session_audio_destroy().

◆ session_mirror_capture_create()

session_capture_ctx_t * session_mirror_capture_create ( const session_capture_config_t *  config)

#include <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 143 of file common/session/capture.c.

143 {
144 // Mirror capture requires a config with media source info
145 if (!config) {
146 return SET_ERRNO(ERROR_INVALID_PARAM, "Mirror capture requires explicit config"), NULL;
147 }
148
149 // Delegate to the main implementation
150 return session_capture_create(config);
151}

References ERROR_INVALID_PARAM, session_capture_create(), and SET_ERRNO.

Referenced by session_client_like_run().

◆ session_network_capture_create()

session_capture_ctx_t * session_network_capture_create ( uint32_t  target_fps)

#include <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 153 of file common/session/capture.c.

153 {
154 // Network modes don't capture media - create minimal context for keyboard/audio only
156 if (!ctx) {
157 return NULL;
158 }
159
160 // Set FPS (use provided value or default to 60)
161 ctx->target_fps = target_fps > 0 ? target_fps : 60;
162 ctx->resize_for_network = false;
163
164 // Audio and keyboard will be set up separately by callers
165 ctx->audio_enabled = false;
166 ctx->source = NULL;
167
168 // Initialize adaptive sleep for consistency
169 uint64_t baseline_sleep_ns = NS_PER_SEC_INT / ctx->target_fps;
170 adaptive_sleep_config_t sleep_config = {.baseline_sleep_ns = baseline_sleep_ns,
171 .min_speed_multiplier = 0.5,
172 .max_speed_multiplier = 2.0,
173 .speedup_rate = 0.1,
174 .slowdown_rate = 0.1};
175 adaptive_sleep_init(&ctx->sleep_state, &sleep_config);
176
177 // Initialize minimal FPS tracker (won't be used but keep structure consistent)
178 char *tracker_name = SAFE_MALLOC(32, char *);
179 if (!tracker_name) {
180 SAFE_FREE(ctx);
181 return NULL;
182 }
183 safe_snprintf(tracker_name, 32, "NETWORK_CAPTURE");
184 fps_init(&ctx->fps_tracker, 60, tracker_name);
185
186 ctx->start_time_ns = time_get_ns();
187 ctx->initialized = true;
188
189 log_debug("Created network capture context (no local media source)");
190 return ctx;
191}

References adaptive_sleep_init(), session_capture_ctx::audio_enabled, adaptive_sleep_config_t::baseline_sleep_ns, fps_init(), session_capture_ctx::fps_tracker, session_capture_ctx::initialized, log_debug, NS_PER_SEC_INT, session_capture_ctx::resize_for_network, SAFE_CALLOC, SAFE_FREE, SAFE_MALLOC, safe_snprintf(), session_capture_ctx::sleep_state, session_capture_ctx::source, session_capture_ctx::start_time_ns, session_capture_ctx::target_fps, and time_get_ns().

Referenced by session_client_like_run().

◆ session_participant_connect()

asciichat_error_t session_participant_connect ( session_participant_t *  p)

#include <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 279 of file participant.c.

279 {
280 log_debug("session_participant_connect: enter - p=%p", p);
281
282 if (!p || !p->initialized) {
283 log_debug("session_participant_connect: invalid param or not initialized");
284 return SET_ERRNO(ERROR_INVALID_PARAM, "session_participant_connect: invalid participant");
285 }
286
287 log_debug("session_participant_connect: p->initialized=%d, p->connected=%d", p->initialized, p->connected);
288
289 if (p->connected) {
290 log_debug("session_participant_connect: already connected, returning OK");
291 return ASCIICHAT_OK; // Already connected
292 }
293
294 // Create TCP connection to server
295 log_debug("session_participant_connect: calling connect_to_server(%s, %d)", p->address, p->port);
296 p->socket = connect_to_server(p->address, p->port);
297 log_debug("session_participant_connect: connect_to_server returned socket=%d (INVALID=%d)", p->socket,
299
300 if (p->socket == INVALID_SOCKET_VALUE) {
301 log_error("Failed to connect to server at %s:%d", p->address, p->port);
302 if (p->callbacks.on_error) {
303 p->callbacks.on_error(p, ERROR_NETWORK_CONNECT, "Failed to connect to server", p->user_data);
304 }
305 return GET_ERRNO();
306 }
307
308 log_debug("session_participant_connect: setting p->connected=true");
309 p->connected = true;
310 p->client_id = 0; // Will be assigned by server
311
312 log_info("Connected to server at %s:%d (socket=%d)", p->address, p->port, p->socket);
313 log_debug("session_participant_connect: p->connected=%d after setting", p->connected);
314
315 // Invoke callback
316 if (p->callbacks.on_connected) {
318 }
319
320 log_debug("session_participant_connect: returning ASCIICHAT_OK");
321 return ASCIICHAT_OK;
322}
@ ERROR_NETWORK_CONNECT
Definition error_codes.h:79
void(* on_connected)(session_participant_t *p, uint32_t client_id, void *user_data)
Called when successfully connected to session.
Definition participant.h:84
void(* on_error)(session_participant_t *p, asciichat_error_t error, const char *message, void *user_data)
Called when an error occurs.
bool connected
Currently connected.
Definition participant.c:80
socket_t socket
Connection socket.
Definition participant.c:74
int port
Server port.
Definition participant.c:50
char address[BUFFER_SIZE_SMALL]
Server address.
Definition participant.c:47
bool initialized
Context is initialized.
session_participant_callbacks_t callbacks
Event callbacks.
Definition participant.c:68
void * user_data
User data for callbacks.
Definition participant.c:71
uint32_t client_id
Assigned client ID.
Definition participant.c:83

References session_participant::address, ASCIICHAT_OK, session_participant::callbacks, session_participant::client_id, session_participant::connected, ERROR_INVALID_PARAM, ERROR_NETWORK_CONNECT, GET_ERRNO, session_participant::initialized, INVALID_SOCKET_VALUE, log_debug, log_error, log_info, session_participant_callbacks_t::on_connected, session_participant_callbacks_t::on_error, session_participant::port, SET_ERRNO, session_participant::socket, and session_participant::user_data.

Referenced by discovery_session_connect_to_future_host(), and discovery_session_process().

◆ session_participant_create()

session_participant_t * session_participant_create ( const session_participant_config_t *  config)

#include <participant.h>

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 123 of file participant.c.

123 {
124 log_info("session_participant_create: START - config=%p", config);
125
126 if (!config) {
127 log_error("session_participant_create: config is NULL");
128 SET_ERRNO(ERROR_INVALID_PARAM, "session_participant_create: NULL config");
129 return NULL;
130 }
131
132 // Allocate participant
134 log_info("session_participant_create: allocated p=%p", p);
135
136 // Copy configuration
137 if (config->address) {
138 SAFE_STRNCPY(p->address, config->address, sizeof(p->address));
139 } else {
140 SAFE_STRNCPY(p->address, "127.0.0.1", sizeof(p->address));
141 }
142
143 p->port = config->port > 0 ? config->port : OPT_PORT_INT_DEFAULT;
145
146 log_info("session_participant_create: address=%s, port=%d", p->address, p->port);
147
148 if (config->password) {
149 SAFE_STRNCPY(p->password, config->password, sizeof(p->password));
150 }
151
152 if (config->server_key) {
153 SAFE_STRNCPY(p->server_key, config->server_key, sizeof(p->server_key));
154 }
155
156 p->enable_audio = config->enable_audio;
157 p->enable_video = config->enable_video;
158 p->callbacks = config->callbacks;
159 p->user_data = config->user_data;
160
161 // Initialize socket to invalid
163 p->transport = NULL; // No alternative transport initially
164 p->connected = false;
165 p->client_id = 0;
166 p->video_active = false;
167 p->audio_active = false;
168
169 // Initialize settings
171
172 p->initialized = true;
173 log_info("session_participant_create: DONE - returning p=%p (initialized=%d, connected=%d)", p, p->initialized,
174 p->connected);
175 return p;
176}
void session_settings_init(session_settings_t *settings)
Initialize session settings to defaults.
Definition settings.c:26
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)
session_settings_t settings
Current session settings.
Definition participant.c:92
struct acip_transport * transport
Alternative transport (WebRTC, WebSocket, etc.) - NULL if using socket only.
Definition participant.c:77
bool audio_active
Audio streaming active.
Definition participant.c:89
bool encryption_enabled
Encryption enabled.
Definition participant.c:53
char password[BUFFER_SIZE_SMALL]
Password (if any)
Definition participant.c:56
bool enable_audio
Audio enabled.
Definition participant.c:62
char server_key[BUFFER_SIZE_MEDIUM]
Server key for verification (if any)
Definition participant.c:59
bool video_active
Video streaming active.
Definition participant.c:86
bool enable_video
Video enabled.
Definition participant.c:65

References session_participant::address, session_participant_config_t::address, session_participant::audio_active, 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, INVALID_SOCKET_VALUE, log_error, log_info, OPT_PORT_INT_DEFAULT, session_participant::password, session_participant_config_t::password, session_participant::port, session_participant_config_t::port, SAFE_CALLOC, SAFE_STRNCPY, session_participant::server_key, session_participant_config_t::server_key, session_settings_init(), SET_ERRNO, session_participant::settings, session_participant::socket, session_participant::transport, session_participant::user_data, session_participant_config_t::user_data, and session_participant::video_active.

Referenced by discovery_session_connect_to_future_host(), and discovery_session_process().

◆ session_participant_destroy()

void session_participant_destroy ( session_participant_t *  p)

#include <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 178 of file participant.c.

178 {
179 if (!p) {
180 return;
181 }
182
183 // Disconnect if connected
184 if (p->connected) {
186 }
187
188 // Stop capture threads if running
189 if (p->video_capture_running) {
191 }
192 if (p->audio_capture_running) {
194 }
195
196 // Clean up media capture contexts
197 if (p->audio_capture) {
199 p->audio_capture = NULL;
200 }
201 if (p->video_capture) {
203 p->video_capture = NULL;
204 }
205 if (p->opus_encoder) {
207 p->opus_encoder = NULL;
208 }
209
210 if (p->transport) {
212 p->transport = NULL;
213 }
214
215 // Close socket if open
216 if (p->socket != INVALID_SOCKET_VALUE) {
219 }
220
221 // Clear sensitive data
222 memset(p->password, 0, sizeof(p->password));
223 memset(p->server_key, 0, sizeof(p->server_key));
224
225 p->initialized = false;
226 SAFE_FREE(p);
227}
void session_participant_stop_video_capture(session_participant_t *p)
Stop video capture and transmission.
void session_participant_stop_audio_capture(session_participant_t *p)
Stop audio capture and transmission.
opus_codec_t * opus_encoder
Opus encoder for audio compression.
session_capture_ctx_t * video_capture
Video capture context (for webcam/file media)
Definition participant.c:95
bool video_capture_running
Video capture thread running flag.
bool audio_capture_running
Audio capture thread running flag.
session_audio_ctx_t * audio_capture
Audio capture context (for microphone)
Definition participant.c:98
void acip_transport_destroy(acip_transport_t *transport)
Destroy transport and free all resources.

References acip_transport_destroy(), session_participant::audio_capture, session_participant::audio_capture_running, session_participant::connected, session_participant::initialized, INVALID_SOCKET_VALUE, opus_codec_destroy(), session_participant::opus_encoder, session_participant::password, SAFE_FREE, session_participant::server_key, session_audio_destroy(), session_capture_destroy(), session_participant_disconnect(), session_participant_stop_audio_capture(), session_participant_stop_video_capture(), session_participant::socket, socket_close(), session_participant::transport, session_participant::video_capture, and session_participant::video_capture_running.

Referenced by discovery_session_connect_to_future_host(), and discovery_session_destroy().

◆ session_participant_disconnect()

void session_participant_disconnect ( session_participant_t *  p)

#include <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 324 of file participant.c.

324 {
325 if (!p || !p->initialized) {
326 return;
327 }
328
329 if (!p->connected) {
330 return;
331 }
332
333 // Stop media streams
334 if (p->video_active) {
336 }
337 if (p->audio_active) {
339 }
340
341 // Close socket
342 if (p->socket != INVALID_SOCKET_VALUE) {
345 }
346
347 p->connected = false;
348 p->client_id = 0;
349
350 // Invoke callback
351 if (p->callbacks.on_disconnected) {
353 }
354}
void session_participant_stop_video(session_participant_t *p)
Stop video capture and streaming.
void session_participant_stop_audio(session_participant_t *p)
Stop audio capture and streaming.
void(* on_disconnected)(session_participant_t *p, void *user_data)
Called when disconnected from session.
Definition participant.h:91

References session_participant::audio_active, session_participant::callbacks, session_participant::client_id, session_participant::connected, session_participant::initialized, INVALID_SOCKET_VALUE, session_participant_callbacks_t::on_disconnected, session_participant_stop_audio(), session_participant_stop_video(), session_participant::socket, socket_close(), session_participant::user_data, and session_participant::video_active.

Referenced by discovery_session_process(), and session_participant_destroy().

◆ session_participant_get_client_id()

uint32_t session_participant_get_client_id ( session_participant_t *  p)

#include <participant.h>

Get assigned client ID.

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

Definition at line 363 of file participant.c.

363 {
364 if (!p || !p->initialized || !p->connected) {
365 return 0;
366 }
367 return p->client_id;
368}

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 <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 473 of file participant.c.

473 {
474 if (!p || !p->initialized || !settings) {
475 return SET_ERRNO(ERROR_INVALID_PARAM, "session_participant_get_settings: invalid parameter");
476 }
477
478 memcpy(settings, &p->settings, sizeof(session_settings_t));
479 return ASCIICHAT_OK;
480}

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

◆ session_participant_get_socket()

socket_t session_participant_get_socket ( session_participant_t *  p)

#include <participant.h>

Get the socket from a participant context.

Parameters
pParticipant handle (must not be NULL)
Returns
The socket file descriptor, or INVALID_SOCKET_VALUE if not connected

Definition at line 370 of file participant.c.

370 {
371 if (!p || !p->initialized) {
373 }
374 return p->socket;
375}

References session_participant::initialized, INVALID_SOCKET_VALUE, and session_participant::socket.

Referenced by discovery_session_check_host_alive(), and discovery_session_process().

◆ session_participant_get_transport()

acip_transport_t * session_participant_get_transport ( session_participant_t *  p)

#include <participant.h>

Get the current transport for the participant.

Returns the currently active transport, if any. May return NULL if only socket transport is in use.

Parameters
pParticipant handle (must not be NULL)
Returns
Transport pointer if set, NULL if using socket transport

Definition at line 770 of file participant.c.

770 {
771 if (!p || !p->initialized) {
772 return NULL;
773 }
774
775 return p->transport;
776}

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

Referenced by discovery_session_process().

◆ session_participant_has_transport()

bool session_participant_has_transport ( session_participant_t *  p)

#include <participant.h>

Check if participant has an active alternative transport.

Convenient way to check if the participant is using an alternative transport (WebRTC, WebSocket, etc.) instead of raw TCP socket.

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

Definition at line 778 of file participant.c.

778 {
779 if (!p) {
780 return false;
781 }
782
783 return p->transport != NULL;
784}

References session_participant::transport.

◆ session_participant_is_audio_active()

bool session_participant_is_audio_active ( session_participant_t *  p)

#include <participant.h>

Check if audio is streaming.

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

Definition at line 462 of file participant.c.

462 {
463 if (!p || !p->initialized) {
464 return false;
465 }
466 return p->audio_active;
467}

References session_participant::audio_active, and session_participant::initialized.

◆ session_participant_is_connected()

bool session_participant_is_connected ( session_participant_t *  p)

#include <participant.h>

Check if participant is connected.

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

Definition at line 356 of file participant.c.

356 {
357 if (!p || !p->initialized) {
358 return false;
359 }
360 return p->connected;
361}

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

Referenced by discovery_session_process().

◆ session_participant_is_video_active()

bool session_participant_is_video_active ( session_participant_t *  p)

#include <participant.h>

Check if video is streaming.

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

Definition at line 418 of file participant.c.

418 {
419 if (!p || !p->initialized) {
420 return false;
421 }
422 return p->video_active;
423}

References session_participant::initialized, and session_participant::video_active.

◆ session_participant_request_settings()

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

#include <participant.h>

Request session settings update (if permitted)

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 the host to update session settings. The host may accept or reject the request.

Note
Actual settings change is signaled via on_settings_changed callback.

Definition at line 482 of file participant.c.

482 {
483 if (!p || !p->initialized || !settings) {
484 return SET_ERRNO(ERROR_INVALID_PARAM, "session_participant_request_settings: invalid parameter");
485 }
486
487 if (!p->connected) {
488 return SET_ERRNO(ERROR_INVALID_STATE, "session_participant_request_settings: not connected");
489 }
490
491 // TODO: Send settings update request to server
492
493 return SET_ERRNO(ERROR_NOT_SUPPORTED, "session_participant_request_settings: not implemented yet");
494}
@ ERROR_NOT_SUPPORTED
Definition error_codes.h:74

References session_participant::connected, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_NOT_SUPPORTED, session_participant::initialized, and SET_ERRNO.

◆ session_participant_set_transport()

asciichat_error_t session_participant_set_transport ( session_participant_t *  p,
acip_transport_t *  transport 
)

#include <participant.h>

Set an alternative transport for the participant.

Allows replacing the default TCP socket transport with an alternative transport such as WebRTC DataChannel. Once set, the participant will use this transport for send/receive operations.

Parameters
pParticipant handle (must not be NULL)
transportAlternative transport to use (can be NULL to clear)
Returns
ASCIICHAT_OK on success, error code on failure
Note
This is used for WebRTC integration when DataChannel becomes ready
Transport ownership remains with caller (participant does not free it)
If both socket and transport exist, transport takes precedence

Definition at line 751 of file participant.c.

751 {
752 if (!p || !p->initialized) {
753 return SET_ERRNO(ERROR_INVALID_PARAM, "Participant is NULL or not initialized");
754 }
755
756 log_info("session_participant_set_transport: Setting transport=%p (was=%p)", transport, p->transport);
757
758 p->transport = transport;
759
760 if (transport) {
761 p->connected = acip_transport_is_connected(transport);
762 log_info("WebRTC transport now active for participant");
763 } else {
764 log_info("WebRTC transport cleared, reverting to socket");
765 }
766
767 return ASCIICHAT_OK;
768}

References ASCIICHAT_OK, session_participant::connected, ERROR_INVALID_PARAM, session_participant::initialized, log_info, SET_ERRNO, and session_participant::transport.

Referenced by discovery_session_process().

◆ session_participant_start_audio()

asciichat_error_t session_participant_start_audio ( session_participant_t *  p)

#include <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.

Definition at line 425 of file participant.c.

425 {
426 if (!p || !p->initialized) {
427 return SET_ERRNO(ERROR_INVALID_PARAM, "session_participant_start_audio: invalid participant");
428 }
429
430 if (!p->connected) {
431 return SET_ERRNO(ERROR_INVALID_STATE, "session_participant_start_audio: not connected");
432 }
433
434 if (p->audio_active) {
435 return ASCIICHAT_OK; // Already active
436 }
437
438 if (!p->enable_audio) {
439 return SET_ERRNO(ERROR_INVALID_STATE, "session_participant_start_audio: audio not enabled");
440 }
441
442 // TODO: Start audio using session_audio_ctx_t
443
444 p->audio_active = true;
445 return ASCIICHAT_OK;
446}

References ASCIICHAT_OK, session_participant::audio_active, session_participant::connected, session_participant::enable_audio, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, session_participant::initialized, and SET_ERRNO.

◆ session_participant_start_audio_capture()

asciichat_error_t session_participant_start_audio_capture ( session_participant_t *  p)

#include <participant.h>

Start audio capture and transmission to host.

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

Spawns a background thread that:

  • Captures audio from microphone
  • Encodes to Opus (lossy compression)
  • Sends AUDIO packets to the host

Audio capture respects the participant's enable_audio flag and connection state. Multiple calls are safe (returns OK if already running).

Note
Requires connection to be established first via session_participant_connect()
Use session_participant_stop_audio_capture() to stop transmission

Definition at line 669 of file participant.c.

669 {
670 if (!p || !p->initialized) {
671 return SET_ERRNO(ERROR_INVALID_PARAM, "session_participant_start_audio_capture: invalid participant");
672 }
673
674 if (!p->connected) {
675 return SET_ERRNO(ERROR_INVALID_STATE, "session_participant_start_audio_capture: not connected");
676 }
677
678 if (!p->enable_audio) {
679 return SET_ERRNO(ERROR_INVALID_STATE, "session_participant_start_audio_capture: audio not enabled");
680 }
681
682 if (p->audio_capture_running) {
683 return ASCIICHAT_OK; // Already running
684 }
685
686 // Create audio capture context if not already created
687 if (!p->audio_capture) {
688 p->audio_capture = session_audio_create(false); // false = participant mode (no mixing)
689 if (!p->audio_capture) {
690 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to create audio capture context");
691 }
692 }
693
695
696 // Start audio capture and playback
698 if (err != ASCIICHAT_OK) {
699 return SET_ERRNO(err, "Failed to start audio duplex");
700 }
701
702 // Create Opus encoder for audio compression (48kHz, VOIP mode, 24 kbps)
703 if (!p->opus_encoder) {
705 if (!p->opus_encoder) {
707 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to create Opus encoder");
708 }
709 }
710
711 // Spawn audio capture thread
712 p->audio_capture_running = true;
713 if (asciichat_thread_create(&p->audio_capture_thread, "audio_capture", participant_audio_capture_thread, p) != 0) {
714 log_error("Failed to spawn audio capture thread");
715 p->audio_capture_running = false;
717 return SET_ERRNO(ERROR_THREAD, "Failed to spawn audio capture thread");
718 }
719
720 log_info("Audio capture started");
721 return ASCIICHAT_OK;
722}
void session_audio_set_capture_source(session_audio_ctx_t *ctx, void *source)
asciichat_thread_t audio_capture_thread
Audio capture thread handle.

References ASCIICHAT_OK, asciichat_thread_create, session_participant::audio_capture, session_participant::audio_capture_running, session_participant::audio_capture_thread, session_participant::connected, session_participant::enable_audio, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_THREAD, session_participant::initialized, log_error, log_info, OPUS_APPLICATION_VOIP, opus_codec_create_encoder(), session_participant::opus_encoder, session_audio_create(), session_audio_set_capture_source(), session_audio_start_duplex(), session_audio_stop(), session_capture_get_media_source(), SET_ERRNO, and session_participant::video_capture.

◆ session_participant_start_video()

asciichat_error_t session_participant_start_video ( session_participant_t *  p)

#include <participant.h>

Start video capture and streaming.

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

Starts capturing video from the webcam and streaming to the server.

Definition at line 381 of file participant.c.

381 {
382 if (!p || !p->initialized) {
383 return SET_ERRNO(ERROR_INVALID_PARAM, "session_participant_start_video: invalid participant");
384 }
385
386 if (!p->connected) {
387 return SET_ERRNO(ERROR_INVALID_STATE, "session_participant_start_video: not connected");
388 }
389
390 if (p->video_active) {
391 return ASCIICHAT_OK; // Already active
392 }
393
394 if (!p->enable_video) {
395 return SET_ERRNO(ERROR_INVALID_STATE, "session_participant_start_video: video not enabled");
396 }
397
398 // TODO: Start webcam capture thread using session_capture_ctx_t
399
400 p->video_active = true;
401 return ASCIICHAT_OK;
402}

References ASCIICHAT_OK, session_participant::connected, session_participant::enable_video, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, session_participant::initialized, SET_ERRNO, and session_participant::video_active.

◆ session_participant_start_video_capture()

asciichat_error_t session_participant_start_video_capture ( session_participant_t *  p)

#include <participant.h>

Start video capture and transmission to host.

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

Spawns a background thread that:

  • Captures video from webcam/file/test pattern
  • Resizes for network efficiency (bandwidth optimization)
  • Sends IMAGE_FRAME packets to the host

Video capture respects the participant's enable_video flag and connection state. Multiple calls are safe (returns OK if already running).

Note
Requires connection to be established first via session_participant_connect()
Use session_participant_stop_video_capture() to stop transmission

Definition at line 595 of file participant.c.

595 {
596 if (!p || !p->initialized) {
597 return SET_ERRNO(ERROR_INVALID_PARAM, "session_participant_start_video_capture: invalid participant");
598 }
599
600 if (!p->connected) {
601 return SET_ERRNO(ERROR_INVALID_STATE, "session_participant_start_video_capture: not connected");
602 }
603
604 if (!p->enable_video) {
605 return SET_ERRNO(ERROR_INVALID_STATE, "session_participant_start_video_capture: video not enabled");
606 }
607
608 if (p->video_capture_running) {
609 return ASCIICHAT_OK; // Already running
610 }
611
612 // Create video capture context if not already created
613 if (!p->video_capture) {
614 session_capture_config_t config = {
616 .path = "0", // Default device
617 .target_fps = 60,
618 .resize_for_network = true, // Optimize for bandwidth
619 };
620 const char *file = GET_OPTION(media_file);
621 const char *url = GET_OPTION(media_url);
622 char webcam_index[32];
623 safe_snprintf(webcam_index, sizeof(webcam_index), "%u", GET_OPTION(webcam_index));
624 config.path = webcam_index;
625 if (url && url[0]) {
626 config.type = MEDIA_SOURCE_FILE;
627 config.path = url;
628 } else if (file && file[0]) {
629 config.type = strcmp(file, "-") == 0 ? MEDIA_SOURCE_STDIN : MEDIA_SOURCE_FILE;
630 config.path = config.type == MEDIA_SOURCE_STDIN ? NULL : file;
631 }
632 config.loop = GET_OPTION(media_loop);
634 if (!p->video_capture) {
635 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to create video capture context");
636 }
637 }
638
639 // Spawn video capture thread (media source is ready in ctx)
640 p->video_capture_running = true;
641 if (asciichat_thread_create(&p->video_capture_thread, "video_capture", participant_video_capture_thread, p) != 0) {
642 log_error("Failed to spawn video capture thread");
643 p->video_capture_running = false;
644 return SET_ERRNO(ERROR_THREAD, "Failed to spawn video capture thread");
645 }
646
647 log_info("Video capture started");
648 return ASCIICHAT_OK;
649}
asciichat_thread_t video_capture_thread
Video capture thread handle.

References ASCIICHAT_OK, asciichat_thread_create, session_participant::connected, session_participant::enable_video, 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_capture_create(), SET_ERRNO, session_capture_config_t::type, session_participant::video_capture, session_participant::video_capture_running, and session_participant::video_capture_thread.

◆ session_participant_stop_audio()

void session_participant_stop_audio ( session_participant_t *  p)

#include <participant.h>

Stop audio capture and streaming.

Parameters
pParticipant handle (must not be NULL)

Definition at line 448 of file participant.c.

448 {
449 if (!p || !p->initialized) {
450 return;
451 }
452
453 if (!p->audio_active) {
454 return;
455 }
456
457 // TODO: Stop audio
458
459 p->audio_active = false;
460}

References session_participant::audio_active, and session_participant::initialized.

Referenced by session_participant_disconnect().

◆ session_participant_stop_audio_capture()

void session_participant_stop_audio_capture ( session_participant_t *  p)

#include <participant.h>

Stop audio capture and transmission.

Parameters
pParticipant handle (must not be NULL)

Stops the audio capture background thread and cleans up resources. Safe to call if audio is not running.

Definition at line 724 of file participant.c.

724 {
725 if (!p || !p->initialized) {
726 return;
727 }
728
729 if (!p->audio_capture_running) {
730 return;
731 }
732
733 // Signal thread to stop
734 p->audio_capture_running = false;
735
736 // Wait for thread to complete
738
739 // Stop audio streams
740 if (p->audio_capture) {
742 }
743
744 log_info("Audio capture stopped");
745}

References asciichat_thread_join, session_participant::audio_capture, session_participant::audio_capture_running, session_participant::audio_capture_thread, session_participant::initialized, log_info, and session_audio_stop().

Referenced by session_participant_destroy().

◆ session_participant_stop_video()

void session_participant_stop_video ( session_participant_t *  p)

#include <participant.h>

Stop video capture and streaming.

Parameters
pParticipant handle (must not be NULL)

Definition at line 404 of file participant.c.

404 {
405 if (!p || !p->initialized) {
406 return;
407 }
408
409 if (!p->video_active) {
410 return;
411 }
412
413 // TODO: Stop webcam capture thread
414
415 p->video_active = false;
416}

References session_participant::initialized, and session_participant::video_active.

Referenced by session_participant_disconnect().

◆ session_participant_stop_video_capture()

void session_participant_stop_video_capture ( session_participant_t *  p)

#include <participant.h>

Stop video capture and transmission.

Parameters
pParticipant handle (must not be NULL)

Stops the video capture background thread and cleans up resources. Safe to call if video is not running.

Definition at line 651 of file participant.c.

651 {
652 if (!p || !p->initialized) {
653 return;
654 }
655
656 if (!p->video_capture_running) {
657 return;
658 }
659
660 // Signal thread to stop
661 p->video_capture_running = false;
662
663 // Wait for thread to complete
665
666 log_info("Video capture stopped");
667}

References asciichat_thread_join, session_participant::initialized, log_info, session_participant::video_capture_running, and session_participant::video_capture_thread.

Referenced by session_participant_destroy().

◆ 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 <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) {
capture, display, my_should_exit,
NULL, NULL, // No custom callbacks
NULL // No user_data needed
);
return result == ASCIICHAT_OK ? 0 : (int)result;
}
bool platform_set_console_ctrl_handler(console_ctrl_handler_t handler)
Register a console control handler (for Ctrl+C, etc.)
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;
}
bool server_connection_is_active()
Check if server connection is currently active.
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.

Definition at line 55 of file common/session/render.c.

58 {
59 // Validate required parameters
60 log_info("[SESSION_RENDER_LOOP] START: capture=%p, display=%p, mode=%s", (void *)capture, (void *)display,
61 capture ? "SYNCHRONOUS" : "EVENT-DRIVEN");
62
63 if (!display) {
64 SET_ERRNO(ERROR_INVALID_PARAM, "session_render_loop: display context is NULL");
66 }
67
68 if (!should_exit) {
69 SET_ERRNO(ERROR_INVALID_PARAM, "session_render_loop: should_exit callback is NULL");
71 }
72
73 // Validate mode: either capture context OR custom callbacks, not both
74 if (!capture && !capture_cb) {
75 SET_ERRNO(ERROR_INVALID_PARAM, "session_render_loop: must provide either capture context or capture callback");
77 }
78
79 if (capture && capture_cb) {
80 SET_ERRNO(ERROR_INVALID_PARAM, "session_render_loop: cannot provide both capture context and capture callback");
82 }
83
84 // In event-driven mode, both capture_cb and sleep_cb must be provided together
85 if (capture_cb && !sleep_cb) {
86 SET_ERRNO(ERROR_INVALID_PARAM, "session_render_loop: capture_cb requires sleep_cb (must provide both or neither)");
88 }
89
90 if (sleep_cb && !capture_cb) {
91 SET_ERRNO(ERROR_INVALID_PARAM, "session_render_loop: sleep_cb requires capture_cb (must provide both or neither)");
93 }
94
95 // SYNCHRONOUS MODE: Use threaded pipeline for capture + render
96 if (capture != NULL) {
97 log_info("[SESSION_RENDER_LOOP_SYNCHRONOUS] Creating pipeline... (capture=%p, display=%p)", (void *)capture,
98 (void *)display);
99 session_pipeline_t *pipeline = NULL;
100 asciichat_error_t pipeline_err = session_pipeline_create(capture, display, &pipeline);
101 log_info("[SESSION_RENDER_LOOP_SYNCHRONOUS] pipeline_create returned: %d (pipeline=%p)", pipeline_err,
102 (void *)pipeline);
103 if (pipeline_err != ASCIICHAT_OK) {
104 log_error("[SESSION_RENDER_LOOP_SYNCHRONOUS] Pipeline creation failed: %d", pipeline_err);
105 return pipeline_err;
106 }
107 log_info("[SESSION_RENDER_LOOP_SYNCHRONOUS] Pipeline created successfully, running main loop...");
108
109 // Disable logging to stdout/stderr during rendering (same as event-driven mode).
110 // This prevents logs from mixing with ASCII art output. Logs still go to file.
112 asciichat_error_t run_err = session_pipeline_run_main(pipeline, should_exit, keyboard_handler, user_data);
114
115 // In snapshot mode, pass actual capture-based elapsed time to encoder for correct video duration
116 if (GET_OPTION(snapshot_mode) && display && g_snapshot_first_capture_ns > 0) {
117 uint64_t now_ns = time_get_ns();
118 uint64_t elapsed_ns = now_ns - g_snapshot_first_capture_ns;
119 double capture_elapsed_sec = (double)elapsed_ns / (double)NS_PER_SEC_INT;
120 log_info("SNAPSHOT: Pipeline finished - passing capture_elapsed=%.3f to encoder", capture_elapsed_sec);
121 session_display_set_snapshot_actual_duration(display, capture_elapsed_sec);
122 }
123
124 session_pipeline_destroy(pipeline);
125 return run_err;
126 } else {
127 log_warn("[SESSION_RENDER_LOOP] capture is NULL - will not use pipeline (capture_cb=%p)", (void *)capture_cb);
128 }
129
130 // Snapshot mode state tracking
131 bool snapshot_mode = GET_OPTION(snapshot_mode);
132 bool snapshot_done = false;
133 uint64_t frames_rendered_since_first = 0; // Track frames rendered after first frame (for snapshot delay)
134 // NOTE: g_snapshot_first_frame_rendered is set by display.c when first frame is rendered via platform_write_all()
135
136 // Help screen state tracking for clear-screen transition
137 bool help_was_active = false;
138
139 // Terminal resize tracking (for auto_width/auto_height mode)
140 unsigned short int last_terminal_width = terminal_get_effective_width();
141 unsigned short int last_terminal_height = terminal_get_effective_height();
142
143 log_info("session_render_loop: STARTING - display=%p capture=%p capture_cb=%p snapshot_mode=%s snapshot_delay=%.2f",
144 (void *)display, (void *)capture, (void *)capture_cb, snapshot_mode ? "YES" : "NO",
145 snapshot_mode ? GET_OPTION(snapshot_delay) : 0.0);
146 log_info("session_render_loop: About to enter main loop, should_exit fn check...");
147 if (should_exit(user_data)) {
148 log_warn("session_render_loop: EXIT signal already set at entry, aborting render loop");
149 return ASCIICHAT_OK;
150 }
151 log_info("session_render_loop: OK to proceed with main loop");
152
153 // Keyboard input initialization (if keyboard handler is provided)
154 // Disable keyboard only in snapshot mode
155 // Keyboard is pre-initialized from asciichat_shared_init()
156 // Allow keyboard input in all other modes if handler provided
157 bool keyboard_enabled = keyboard_handler && !snapshot_mode;
158
159 // Pause mode state tracking (for pause/unpause transitions in event-driven mode)
160 bool initial_paused_frame_rendered = false;
161 bool is_paused = false;
162 log_info("render_loop: Keyboard setup - handler=%p snapshot=%s enabled=%s", (void *)keyboard_handler,
163 snapshot_mode ? "YES" : "NO", keyboard_enabled ? "YES" : "NO");
164
165 // Log TTY detection status for debugging keyboard issues
166 log_info("TTY Status: stdin_tty=%d stdout_tty=%d interactive=%d keyboard_enabled=%d", terminal_is_stdin_tty(),
167 terminal_is_stdout_tty(), terminal_is_interactive(), keyboard_enabled);
168
169 // Frame rate timing (event-driven mode only)
170 uint64_t frame_count = 0;
171 uint64_t frame_start_ns = 0;
172
173 // Event-driven mode: disable console logging during rendering to prevent logs from corrupting frame display
174 // This must be done BEFORE the render loop to avoid repeated lock/unlock cycles that could cause deadlocks
175 log_info("[EVENT_DRIVEN_MODE] Entering render loop. snapshot_mode=%s", snapshot_mode ? "YES" : "NO");
177
178 // Main render loop
179 log_debug("session_render_loop: entering main loop");
180 int loop_iteration = 0;
181 log_info("[RENDER_LOOP_START] snapshot_mode=%s, snapshot_delay=%.2f", snapshot_mode ? "YES" : "NO",
182 snapshot_mode ? GET_OPTION(snapshot_delay) : 0.0);
183
184 // Wait for splash animation to complete before rendering first frame
185 // This prevents ASCII art from mixing with splash screen output
186 // But skip for immediate snapshots (snapshot-delay=0) which need to output first frame immediately
187 static bool splash_wait_done = false;
188 if (!splash_wait_done) {
189 splash_wait_done = true;
190 if (!(GET_OPTION(snapshot_mode) && GET_OPTION(snapshot_delay) == 0.0)) {
191 // Re-enable logging briefly for splash wait, then disable again
195 }
196 }
197
198 // In snapshot mode, we must render at least one frame before checking should_exit
199 // to ensure the snapshot delay timer can start
200 bool force_first_iteration = snapshot_mode && frame_count == 0;
201
202 // Register display context globally so signal handlers (Ctrl+C) can access it
203 // This enables Ctrl+C to close the help screen instead of quitting when help is active
205
206 while (force_first_iteration || (!should_exit(user_data) && !(snapshot_mode && snapshot_done))) {
207 force_first_iteration = false;
208 loop_iteration++;
209 log_info("[LOOP_ITER] iteration=%d, snapshot_done=%s, frame_count=%lu", loop_iteration,
210 snapshot_done ? "YES" : "NO", frame_count);
211 if (loop_iteration % 60 == 0) {
212 log_debug("session_render_loop: iteration %d, should_exit check returning false", loop_iteration);
213 }
214 // Snapshot mode: exit at start of iteration if done
215 // This prevents frame 2+ from being captured when snapshot_delay has elapsed
216 if (snapshot_mode && snapshot_done && frame_count > 0) {
217 log_info("[SNAPSHOT_EXIT] Snapshot mode: exiting at loop iteration start (snapshot_done=true, frames=%lu)",
218 frame_count);
219 break;
220 }
221
222 log_debug_every(US_PER_SEC_INT, "session_render_loop: frame %lu", frame_count);
223 // Frame timing - measure total time to maintain target FPS
224 frame_start_ns = time_get_ns();
225
226 // Frame capture and timing - mode-dependent
227 image_t *image = NULL;
228 uint64_t capture_start_ns = 0;
229 uint64_t capture_end_ns = 0;
230 uint64_t pre_convert_ns = 0;
231 uint64_t post_convert_ns = 0;
232 uint64_t conversion_elapsed_ns = 0;
233
234 // EVENT-DRIVEN MODE: Frames come from callbacks
235 // Both sleep_cb and capture_cb are guaranteed non-NULL by validation above
236 sleep_cb(user_data);
237 image = capture_cb(user_data);
238
239 if (!image) {
240 // No frame available - this is normal in async modes (network latency, etc.)
241 // Just continue to next iteration, don't exit
242 continue;
243 }
244
245 // Event-driven mode: increment frame count
246 frame_count++;
247
248 // Check for terminal resize (if auto_width or auto_height is enabled)
249 // This allows the render to adapt immediately when the user resizes the terminal
252 if (auto_width || auto_height) {
253 unsigned short int current_width = 0;
254 unsigned short int current_height = 0;
255
256 asciichat_error_t size_err = get_terminal_size(&current_width, &current_height);
257 if (size_err == ASCIICHAT_OK) {
258 bool width_changed = auto_width && (current_width != last_terminal_width);
259 bool height_changed = auto_height && (current_height != last_terminal_height);
260
261 if (width_changed || height_changed) {
262 if (width_changed) {
263 options_set_int("width", current_width);
264 log_info("Terminal width changed: %u → %u", last_terminal_width, current_width);
265 last_terminal_width = current_width;
266 }
267 if (height_changed) {
268 options_set_int("height", current_height);
269 log_info("Terminal height changed: %u → %u", last_terminal_height, current_height);
270 last_terminal_height = current_height;
271 }
272 // Clear screen when terminal is resized to avoid visual artifacts
274 }
275 }
276 }
277
278 // Convert image to ASCII using display context
279 // Handles all palette, terminal caps, width, height, stretch settings
280 pre_convert_ns = time_get_ns();
281 char *ascii_frame = session_display_convert_to_ascii(display, image);
282 post_convert_ns = time_get_ns();
283 conversion_elapsed_ns = post_convert_ns - pre_convert_ns;
284
285 if (frame_count <= 3) {
286 log_info("RENDER_LOOP[%lu]: image=%p, ascii_frame=%p (conversion took %u ms)", frame_count, (void *)image,
287 (void *)ascii_frame, (unsigned)(conversion_elapsed_ns / NS_PER_MS_INT));
288 }
289
290 // Declare variables that need scope beyond the if (ascii_frame) block
291 bool is_paused_frame = initial_paused_frame_rendered && is_paused;
292 bool output_paused_frame = snapshot_mode && is_paused_frame;
293 uint64_t pre_render_ns = 0, post_render_ns = 0;
294
295 if (ascii_frame) {
296 log_info_every(1 * NS_PER_SEC_INT, "render_loop: ascii_frame ready (len=%zu)", strlen(ascii_frame));
297
298 // Always attempt to render frames; the display context will handle filtering based on:
299 // - TTY mode: render all frames with cursor control (even in snapshot mode, for animation)
300 // - Piped mode: render all frames without cursor control (for continuous capture)
301 // - Snapshot mode on non-TTY: only the display context renders the final frame
302 // BUT: don't render if splash screen is still animating - wait for it to finish
303 bool splash_running = splash_is_running();
304 bool should_write = !splash_running;
305 if (frame_count == 1) {
306 log_dev("[RENDER_FRAME] Frame 1: splash_is_running=%s, should_write=%s", splash_running ? "YES" : "NO",
307 should_write ? "YES" : "NO");
308 }
309 if (should_write) {
310 // is_final = true when: snapshot done, or paused frame (for both snapshot and pause modes)
311 // Profile: render frame
312 pre_render_ns = time_get_ns();
313 START_TIMER("render_frame");
314
315 if (frame_count <= 5 || frame_count % 10 == 0) {
316 log_info("RENDER_LOOP: Frame %lu - calling session_display_render_frame, display=%p",
317 (unsigned long)frame_count, (void *)display);
318 }
319
320 // Check if SIGINT handler cancelled help mode (Ctrl+C while help is open).
321 // The signal handler sets an atomic flag instead of touching state directly.
322 if (display && keyboard_help_check_signal_cancel()) {
323 keyboard_help_toggle(display);
324 log_debug("Ctrl+C cancelled help screen - returning to ASCII art");
325 }
326
327 // Check if help screen is active - if so, render help instead of frame
328 // Help screen is disabled in snapshot mode and non-interactive terminals (keyboard disabled)
329 bool help_is_active = display && keyboard_help_is_active(display);
330
331 // Detect transition from help to ASCII art rendering
332 // When help closes, clear the screen before rendering ASCII art
333 if (help_was_active && !help_is_active) {
335 log_debug_every(1 * NS_PER_SEC_INT, "Cleared screen when transitioning from help to ASCII art");
336 }
337
338 if (help_is_active) {
339 keyboard_help_render(display);
340 } else {
341 session_display_render_frame(display, ascii_frame);
342 }
343
344 // Snapshot mode: increment frame counter for all displayed frames
345 // Counter starts at 1 for the first displayed frame, then 2, 3, 4... for subsequent frames
346 // Timer is started by display.c when g_snapshot_first_frame_rendered is set
347 if (snapshot_mode && g_snapshot_first_frame_rendered) {
348 frames_rendered_since_first++;
349 log_info_every(1 * NS_PER_SEC_INT, "[SNAPSHOT] Frame rendered: frames_rendered_since_first=%lu",
350 frames_rendered_since_first);
351 }
352
353 // Update help state for next iteration
354 help_was_active = help_is_active;
355
356 STOP_TIMER("render_frame");
357 }
358
359 // Keyboard input polling (if enabled) - MUST come before snapshot exit check so help screen can be toggled
360 // Allow keyboard in snapshot mode too (for help screen toggle debugging)
361 // Only enable keyboard if BOTH stdin AND stdout are TTYs to avoid buffering issues
362 // when tcsetattr() modifies the tty line discipline
363 if (keyboard_enabled && keyboard_handler) {
364 START_TIMER("keyboard_read_%lu", (unsigned long)frame_count);
366 double keyboard_elapsed_ns = STOP_TIMER("keyboard_read_%lu", (unsigned long)frame_count);
367 if (keyboard_elapsed_ns >= 0.0) {
368 char _duration_str[32];
369 time_pretty((uint64_t)keyboard_elapsed_ns, -1, _duration_str, sizeof(_duration_str));
370 log_dev_every(1 * NS_PER_SEC_INT, "RENDER[%lu] Keyboard read complete (key=%d) in %s",
371 (unsigned long)frame_count, key, _duration_str);
372 }
373 if (key != KEY_NONE) {
374 log_debug("KEYBOARD: Key pressed: code=%d char='%c'", key, (key >= 32 && key < 127) ? key : '?');
375 // Check if interactive grep should handle this key
376 if (log_search_should_handle(key)) {
377 log_debug("KEYBOARD: Grep handler taking key %d", key);
379 continue; // Force immediate re-render
380 }
381
382 // Normal keyboard handler
383 log_debug("KEYBOARD: Normal handler taking key %d", key);
384 keyboard_handler(capture, key, user_data);
385 }
386 }
387
388 // Free frame before checking exit conditions to avoid double-free
389 SAFE_FREE(ascii_frame);
390 }
391
392 // Snapshot mode: check if elapsed time has reached snapshot_delay duration (terminal render timing)
393 // Separate from capture timing - both should measure their own snapshot_delay
394 if (snapshot_mode && !snapshot_done) {
396 double snapshot_delay = GET_OPTION(snapshot_delay);
397 uint64_t now_ns = time_get_ns();
398 uint64_t elapsed_ns = now_ns - g_snapshot_first_frame_rendered_ns;
399 double elapsed_sec = (double)elapsed_ns / (double)NS_PER_SEC_INT;
400
401 if (frames_rendered_since_first == 1 || frames_rendered_since_first % 20 == 0) {
402 log_info("SNAPSHOT: RENDER_CHECK iter=%lu elapsed=%.3f target=%.2f (from first_frame_rendered)",
403 frames_rendered_since_first, elapsed_sec, snapshot_delay);
404 }
405
406 // snapshot_delay=0 means exit after first frame
407 // snapshot_delay>0 means wait that many seconds before exiting
408 // Exit is based on elapsed wall-clock time, not frame count
409 bool should_exit = (snapshot_delay == 0.0) || (elapsed_sec >= snapshot_delay);
410
411 if (should_exit) {
412 log_info("SNAPSHOT: RENDER_DONE at iteration %lu - elapsed=%.3f target=%.2f (setting snapshot_done=true)",
413 frames_rendered_since_first, elapsed_sec, snapshot_delay);
414 // We don't end frames with newlines so the next log would print on the same line as the frame's
415 // last row without an \n here. We only need this \n in stdout when interactive,
416 // so piped snapshots don't have a weird newline in stdout that they don't need.
418 printf("\n");
419 }
420 snapshot_done = true;
421 }
422 } else {
423 if (frames_rendered_since_first == 0) {
424 log_debug("SNAPSHOT: Waiting for first_frame_rendered (g_snapshot_first_frame_rendered=%d, "
425 "g_snapshot_first_frame_rendered_ns=%llu)",
427 }
428 }
429 }
430
431 // Exit conditions: snapshot mode exits after capturing the final frame or initial paused frame
432 if (snapshot_mode && (snapshot_done || output_paused_frame)) {
433 log_info("SNAPSHOT: EXIT CONDITION MET - snapshot_done=%d, output_paused_frame=%d", snapshot_done,
434 output_paused_frame);
435
436 // Calculate elapsed time from first frame captured (for video file)
437 // This is separate from render time - video should span from first capture to last capture
438 double capture_elapsed_sec = 0.0;
440 uint64_t now_ns = time_get_ns();
441 uint64_t elapsed_ns = now_ns - g_snapshot_first_capture_ns;
442 capture_elapsed_sec = (double)elapsed_ns / (double)NS_PER_SEC_INT;
443 log_info("SNAPSHOT: EXIT - Capture elapsed: %.3f seconds, render elapsed: %.3f seconds, "
444 "g_snapshot_first_capture_ns=%llu",
445 capture_elapsed_sec,
448 : 0.0,
449 (unsigned long long)g_snapshot_first_capture_ns);
450 } else {
451 log_warn("SNAPSHOT: EXIT condition met but g_snapshot_first_capture_ns is 0!");
452 }
453
454 // Pass actual capture elapsed time to encoder for correct video duration
455 log_info("SNAPSHOT: About to call session_display_set_snapshot_actual_duration with duration=%.3f, display=%p",
456 capture_elapsed_sec, (void *)display);
457 if (display) {
458 session_display_set_snapshot_actual_duration(display, capture_elapsed_sec);
459 } else {
460 log_warn("SNAPSHOT: display context is NULL!");
461 }
462
463 // Signal application to exit in snapshot mode
465 break;
466 }
467
468 // Frame rendering and timing details (moved down since snapshot exit is now above it)
469 if (ascii_frame) {
470
471 // Measure frame completion right after rendering, BEFORE keyboard polling
472 // This gives us accurate timing for just the core frame operations
473 uint64_t frame_end_render_ns = time_get_ns();
474
475 // Calculate each phase duration
476 uint64_t prestart_ms =
477 (capture_start_ns > frame_start_ns) ? (capture_start_ns - frame_start_ns) / NS_PER_MS_INT : 0;
478 uint64_t capture_ms =
479 (capture_end_ns > capture_start_ns) ? (capture_end_ns - capture_start_ns) / NS_PER_MS_INT : 0;
480 uint64_t convert_ms = conversion_elapsed_ns / NS_PER_MS_INT;
481 uint64_t render_ms =
482 (post_render_ns > pre_render_ns && post_render_ns > 0) ? (post_render_ns - pre_render_ns) / NS_PER_MS_INT : 0;
483 uint64_t total_ms =
484 (frame_end_render_ns > frame_start_ns) ? (frame_end_render_ns - frame_start_ns) / NS_PER_MS_INT : 0;
485
486 // Log phase breakdown every 5 frames
487 if (frame_count % 5 == 0) {
489 2 * NS_PER_SEC_INT,
490 "PHASE_BREAKDOWN[%lu]: prestart=%llu ms, capture=%llu ms, convert=%llu ms, render=%llu ms (total=%llu ms)",
491 frame_count, prestart_ms, capture_ms, convert_ms, render_ms, total_ms);
492 }
493 } else {
494 // Snapshot mode: even if frame conversion failed, check if we should exit
495 // This ensures snapshot_delay is honored even if display context isn't rendering
496 if (snapshot_mode && snapshot_done) {
497 break;
498 }
499 }
500
501 // In event-driven mode, frame rate limiting is handled by the sleep_cb callback
502 // Synchronous mode (with capture context) uses the pipeline and returns early above
503
504 // Note: Images returned by media sources are cached/reused and should NOT be destroyed
505 // The image pointers are managed by the source and will be cleaned up on source shutdown
506 } // while (!should_exit(user_data)) {
507
508 // Re-enable console logging after rendering completes
510 if (!snapshot_mode && terminal_is_interactive()) {
511 printf("\n");
512 }
513
514 // Keyboard input cleanup (if it was initialized)
515 if (keyboard_enabled) {
517 log_debug_every(2 * NS_PER_SEC_INT, "Keyboard input disabled");
518 }
519
520 // Unregister global display context (for signal handlers)
522
523 return ASCIICHAT_OK;
524}
#define APP_CALLBACK_VOID(callback_name)
bool g_snapshot_first_frame_rendered
Snapshot mode global timing variables Used to track actual duration when snapshot mode is active.
uint64_t g_snapshot_first_capture_ns
void log_set_terminal_output(bool enabled)
Control stderr output to terminal.
Definition log/log.c:700
#define STOP_TIMER(name_fmt,...)
Stop a timer with formatted name and return elapsed time.
Definition time.h:403
keyboard_key_t keyboard_read_nonblocking(void)
Read next keyboard input without blocking.
void keyboard_destroy(void)
Cleanup keyboard input system and restore terminal.
keyboard_key_t
Unified keyboard key code enumeration.
Definition keyboard.h:54
bool splash_is_running(void)
Check if splash screen animation is currently running.
Definition splash.c:725
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:731
#define log_info_every(interval_us, fmt,...)
Rate-limited INFO logging.
Definition log/log.h:705
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:702
void signal_exit(void)
Definition misc.c:129
bool should_exit(void)
Definition misc.c:131
bool auto_height
bool auto_width
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:474
asciichat_error_t session_pipeline_create(session_capture_ctx_t *capture, session_display_ctx_t *display, session_pipeline_t **out)
Definition pipeline.c:420
asciichat_error_t session_pipeline_destroy(session_pipeline_t *pipeline)
Definition pipeline.c:557
asciichat_error_t get_terminal_size(unsigned short int *width, unsigned short int *height)
Get terminal size with multiple fallback methods.
bool log_search_should_handle(int key)
Check if a key should be handled by grep module.
Definition search.c:399
asciichat_error_t log_search_handle_key(keyboard_key_t key)
Process keyboard input for grep.
Definition search.c:414

References APP_CALLBACK_VOID, ASCIICHAT_OK, auto_height, auto_width, ERROR_INVALID_PARAM, g_snapshot_first_capture_ns, g_snapshot_first_frame_rendered, g_snapshot_first_frame_rendered_ns, GET_OPTION, get_terminal_size(), KEY_NONE, keyboard_destroy(), keyboard_help_check_signal_cancel(), keyboard_help_is_active(), keyboard_help_render(), keyboard_help_toggle(), keyboard_read_nonblocking(), log_debug, log_debug_every, log_dev, log_dev_every, log_error, log_info, log_info_every, log_search_handle_key(), log_search_should_handle(), log_set_terminal_output(), log_warn, NS_PER_MS_INT, NS_PER_SEC_INT, options_set_int(), SAFE_FREE, session_display_convert_to_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(), START_TIMER, STOP_TIMER, terminal_clear_screen(), terminal_get_effective_height(), terminal_get_effective_width(), terminal_is_interactive(), terminal_is_stdin_tty(), terminal_is_stdout_tty(), time_get_ns(), time_pretty(), and US_PER_SEC_INT.

◆ session_settings_apply_to_options()

asciichat_error_t session_settings_apply_to_options ( const session_settings_t *  settings)

#include <settings.h>

Apply settings to global options.

Parameters
settingsSettings to apply (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Updates the global options system with values from the settings structure. Only modifies options that are represented in settings.

Definition at line 198 of file settings.c.

198 {
199 if (!settings) {
200 return SET_ERRNO(ERROR_INVALID_PARAM, "session_settings_apply_to_options: NULL settings");
201 }
202
203 // Update dimensions if specified
204 if (settings->width > 0 && settings->height > 0) {
205 asciichat_error_t width_result = options_set_int("width", (int)settings->width);
206 asciichat_error_t height_result = options_set_int("height", (int)settings->height);
207 if (width_result != ASCIICHAT_OK || height_result != ASCIICHAT_OK) {
208 log_warn("Failed to apply dimension settings: width=%d, height=%d", width_result, height_result);
209 }
210 }
211
212 // Note: Other options would require additional RCU update functions
213 // For now, dimensions are the primary use case for runtime updates
214 // Color mode, render mode, and palette typically don't change mid-session
215
216 return ASCIICHAT_OK;
217}
int16_t width
Terminal width in characters (0 = auto-detect)
Definition settings.h:82
int16_t height
Terminal height in characters (0 = auto-detect)
Definition settings.h:85

References ASCIICHAT_OK, ERROR_INVALID_PARAM, session_settings_t::height, log_warn, options_set_int(), SET_ERRNO, and session_settings_t::width.

◆ session_settings_deserialize()

asciichat_error_t session_settings_deserialize ( const uint8_t *  buffer,
size_t  len,
session_settings_t *  settings 
)

#include <settings.h>

Deserialize session settings from binary buffer.

Parameters
bufferInput buffer containing serialized settings
lenLength of input buffer in bytes
settingsOutput settings structure (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Deserializes settings from binary format. Validates buffer length and extracts fields in network byte order.

Note
Returns ERROR_INVALID_PARAM if buffer is too small.

Definition at line 101 of file settings.c.

101 {
102 if (!buffer || !settings) {
103 return SET_ERRNO(ERROR_INVALID_PARAM, "session_settings_deserialize: NULL parameter");
104 }
105
106 // Check minimum buffer size
108 return SET_ERRNO(ERROR_INVALID_PARAM, "session_settings_deserialize: buffer too small (%zu < %d)", len,
110 }
111
112 memset(settings, 0, sizeof(session_settings_t));
113 size_t offset = 0;
114
115 // Version (4 bytes, network byte order)
116 uint32_t version_net;
117 memcpy(&version_net, buffer + offset, sizeof(version_net));
118 settings->version = ntohl(version_net);
119 offset += sizeof(version_net);
120
121 // Width (2 bytes, network byte order)
122 uint16_t width_net;
123 memcpy(&width_net, buffer + offset, sizeof(width_net));
124 settings->width = (int16_t)ntohs(width_net);
125 offset += sizeof(width_net);
126
127 // Height (2 bytes, network byte order)
128 uint16_t height_net;
129 memcpy(&height_net, buffer + offset, sizeof(height_net));
130 settings->height = (int16_t)ntohs(height_net);
131 offset += sizeof(height_net);
132
133 // Color mode (1 byte)
134 settings->color_mode = buffer[offset++];
135
136 // Render mode (1 byte)
137 settings->render_mode = buffer[offset++];
138
139 // Palette type (1 byte)
140 settings->palette_type = buffer[offset++];
141
142 // Custom palette (32 bytes)
143 memcpy(settings->palette_custom, buffer + offset, 32);
144 settings->palette_custom[31] = '\0'; // Ensure null termination
145 offset += 32;
146
147 // Audio enabled (1 byte)
148 settings->audio_enabled = buffer[offset++];
149
150 // Encryption required (1 byte)
151 settings->encryption_required = buffer[offset++];
152
153 // Reserved (16 bytes)
154 memcpy(settings->reserved, buffer + offset, 16);
155 // offset += 16; // Unused, commented to avoid warning
156
157 return ASCIICHAT_OK;
158}
unsigned short uint16_t
Definition common.h:57
uint8_t audio_enabled
Audio enabled flag.
Definition settings.h:100
char palette_custom[32]
Custom palette characters (if palette_type == PALETTE_CUSTOM)
Definition settings.h:97
uint8_t color_mode
Color mode (terminal_color_mode_t value)
Definition settings.h:88
uint8_t palette_type
Palette type (palette_type_t value)
Definition settings.h:94
uint8_t encryption_required
Encryption required flag.
Definition settings.h:103
uint8_t render_mode
Render mode (render_mode_t value)
Definition settings.h:91
uint8_t reserved[16]
Reserved bytes for future expansion.
Definition settings.h:106

References ASCIICHAT_OK, session_settings_t::audio_enabled, session_settings_t::color_mode, session_settings_t::encryption_required, ERROR_INVALID_PARAM, session_settings_t::height, session_settings_t::palette_custom, session_settings_t::palette_type, session_settings_t::render_mode, session_settings_t::reserved, SESSION_SETTINGS_SERIALIZED_SIZE, SET_ERRNO, session_settings_t::version, and session_settings_t::width.

◆ session_settings_equal()

bool session_settings_equal ( const session_settings_t *  a,
const session_settings_t *  b 
)

#include <settings.h>

Compare two settings structures for equality.

Parameters
aFirst settings structure (must not be NULL)
bSecond settings structure (must not be NULL)
Returns
true if settings are equal (ignoring version), false otherwise

Definition at line 224 of file settings.c.

224 {
225 if (!a || !b) {
226 return false;
227 }
228
229 // Compare all fields except version
230 return a->width == b->width && a->height == b->height && a->color_mode == b->color_mode &&
231 a->render_mode == b->render_mode && a->palette_type == b->palette_type &&
232 strncmp(a->palette_custom, b->palette_custom, sizeof(a->palette_custom)) == 0 &&
234}

References session_settings_t::audio_enabled, session_settings_t::color_mode, session_settings_t::encryption_required, session_settings_t::height, session_settings_t::palette_custom, session_settings_t::palette_type, session_settings_t::render_mode, and session_settings_t::width.

◆ session_settings_from_options()

asciichat_error_t session_settings_from_options ( session_settings_t *  settings)

#include <settings.h>

Populate settings from current global options.

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

Reads current values from the global options system and populates the settings structure. Version is set to current timestamp.

Definition at line 160 of file settings.c.

160 {
161 if (!settings) {
162 return SET_ERRNO(ERROR_INVALID_PARAM, "session_settings_from_options: NULL settings");
163 }
164
165 // Initialize to defaults
166 session_settings_init(settings);
167
168 // Get current options
169 const options_t *opts = options_get();
170 if (!opts) {
171 return SET_ERRNO(ERROR_CONFIG, "Options not initialized");
172 }
173
174 // Set version to current time for ordering
175 settings->version = (uint32_t)time(NULL);
176
177 // Copy dimension settings
178 settings->width = (int16_t)opts->width;
179 settings->height = (int16_t)opts->height;
180
181 // Copy display settings
182 settings->color_mode = (uint8_t)opts->color_mode;
183 settings->render_mode = (uint8_t)opts->render_mode;
184 settings->palette_type = (uint8_t)opts->palette_type;
185
186 // Copy custom palette if set
187 if (opts->palette_custom_set && opts->palette_custom[0] != '\0') {
188 SAFE_STRNCPY(settings->palette_custom, opts->palette_custom, sizeof(settings->palette_custom));
189 }
190
191 // Copy audio/encryption settings
192 settings->audio_enabled = (uint8_t)opts->audio_enabled;
193 settings->encryption_required = opts->no_encrypt ? 0 : 1;
194
195 return ASCIICHAT_OK;
196}
@ ERROR_CONFIG
Definition error_codes.h:57
const options_t * options_get(void)
Get current options (lock-free read)
Definition rcu.c:496
Consolidated options structure.
terminal_color_mode_t color_mode
Color mode (auto/none/16/256/truecolor)
int height
Terminal height in characters (int for OPTION_TYPE_INT compat)
int width
Terminal width in characters (int for OPTION_TYPE_INT compat)

References ASCIICHAT_OK, options_state::audio_enabled, session_settings_t::audio_enabled, options_state::color_mode, session_settings_t::color_mode, session_settings_t::encryption_required, ERROR_CONFIG, ERROR_INVALID_PARAM, options_state::height, session_settings_t::height, options_state::no_encrypt, options_get(), options_state::palette_custom, session_settings_t::palette_custom, options_state::palette_custom_set, options_state::palette_type, session_settings_t::palette_type, options_state::render_mode, session_settings_t::render_mode, SAFE_STRNCPY, session_settings_init(), SET_ERRNO, session_settings_t::version, options_state::width, and session_settings_t::width.

◆ session_settings_init()

void session_settings_init ( session_settings_t *  settings)

#include <settings.h>

Initialize session settings to defaults.

Parameters
settingsSettings structure to initialize (must not be NULL)

Initializes settings structure with sensible defaults:

  • Width/height: 0 (auto-detect)
  • Color mode: TERM_COLOR_AUTO
  • Render mode: RENDER_MODE_FOREGROUND
  • Palette: PALETTE_STANDARD
  • Audio: disabled
  • Encryption: required

Definition at line 26 of file settings.c.

26 {
27 if (!settings) {
28 return;
29 }
30
31 memset(settings, 0, sizeof(session_settings_t));
32
33 // Read all defaults from the RCU options system
34 settings->version = 0;
35 settings->width = (int16_t)terminal_get_effective_width();
36 settings->height = (int16_t)terminal_get_effective_height();
37 settings->color_mode = (uint8_t)GET_OPTION(color_mode);
38 settings->render_mode = (uint8_t)GET_OPTION(render_mode);
39 settings->palette_type = (uint8_t)GET_OPTION(palette_type);
40
41 // Custom palette
42 const char *palette = GET_OPTION(palette_custom);
43 if (palette && palette[0] != '\0') {
44 SAFE_STRNCPY(settings->palette_custom, palette, sizeof(settings->palette_custom));
45 }
46
47 settings->audio_enabled = (uint8_t)GET_OPTION(audio_enabled);
48 settings->encryption_required = GET_OPTION(no_encrypt) ? 0 : 1;
49}

References session_settings_t::audio_enabled, session_settings_t::color_mode, session_settings_t::encryption_required, GET_OPTION, session_settings_t::height, session_settings_t::palette_custom, session_settings_t::palette_type, session_settings_t::render_mode, SAFE_STRNCPY, terminal_get_effective_height(), terminal_get_effective_width(), session_settings_t::version, and session_settings_t::width.

Referenced by session_participant_create(), and session_settings_from_options().

◆ session_settings_needs_update()

bool session_settings_needs_update ( uint32_t  local_version,
uint32_t  remote_version 
)

#include <settings.h>

Check if settings need update based on versions.

Parameters
local_versionCurrent local settings version
remote_versionReceived remote settings version
Returns
true if local should update to remote, false otherwise

Determines if local settings should be updated based on version comparison. Higher version number wins (newer settings).

Definition at line 219 of file settings.c.

219 {
220 // Higher version wins (newer settings)
221 return remote_version > local_version;
222}

◆ session_settings_serialize()

asciichat_error_t session_settings_serialize ( const session_settings_t *  settings,
uint8_t *  buffer,
size_t *  len 
)

#include <settings.h>

Serialize session settings to binary buffer.

Parameters
settingsSettings to serialize (must not be NULL)
bufferOutput buffer (must be at least SESSION_SETTINGS_SERIALIZED_SIZE bytes)
lenOutput parameter for actual serialized length (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Serializes settings to a compact binary format suitable for network transmission. Uses network byte order for multi-byte integers.

Definition at line 51 of file settings.c.

51 {
52 if (!settings || !buffer || !len) {
53 return SET_ERRNO(ERROR_INVALID_PARAM, "session_settings_serialize: NULL parameter");
54 }
55
56 // Fixed-size serialization format
57 size_t offset = 0;
58
59 // Version (4 bytes, network byte order)
60 uint32_t version_net = htonl(settings->version);
61 memcpy(buffer + offset, &version_net, sizeof(version_net));
62 offset += sizeof(version_net);
63
64 // Width (2 bytes, network byte order)
65 uint16_t width_net = htons((uint16_t)(settings->width & 0xFFFF));
66 memcpy(buffer + offset, &width_net, sizeof(width_net));
67 offset += sizeof(width_net);
68
69 // Height (2 bytes, network byte order)
70 uint16_t height_net = htons((uint16_t)(settings->height & 0xFFFF));
71 memcpy(buffer + offset, &height_net, sizeof(height_net));
72 offset += sizeof(height_net);
73
74 // Color mode (1 byte)
75 buffer[offset++] = settings->color_mode;
76
77 // Render mode (1 byte)
78 buffer[offset++] = settings->render_mode;
79
80 // Palette type (1 byte)
81 buffer[offset++] = settings->palette_type;
82
83 // Custom palette (32 bytes, null-padded)
84 memcpy(buffer + offset, settings->palette_custom, 32);
85 offset += 32;
86
87 // Audio enabled (1 byte)
88 buffer[offset++] = settings->audio_enabled;
89
90 // Encryption required (1 byte)
91 buffer[offset++] = settings->encryption_required;
92
93 // Reserved (16 bytes)
94 memcpy(buffer + offset, settings->reserved, 16);
95 offset += 16;
96
97 *len = offset;
98 return ASCIICHAT_OK;
99}

References ASCIICHAT_OK, session_settings_t::audio_enabled, session_settings_t::color_mode, session_settings_t::encryption_required, ERROR_INVALID_PARAM, session_settings_t::height, session_settings_t::palette_custom, session_settings_t::palette_type, session_settings_t::render_mode, session_settings_t::reserved, SET_ERRNO, session_settings_t::version, and session_settings_t::width.

◆ splash_clear_display_context()

void splash_clear_display_context ( void  )

#include <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 721 of file splash.c.

721 {
722 // No-op: display context is no longer cached
723}

Referenced by session_display_destroy().

◆ splash_intro_done()

int splash_intro_done ( void  )

#include <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 693 of file splash.c.

693 {
694 // Record when intro_done was called - animation thread will use this to decide when to stop
695 // Animation thread needs time to check the flag and perform cleanup, so this must happen
696 // BEFORE we try to access the terminal for frame rendering
697 atomic_store_u64(&g_splash_state.intro_done_time_ns, time_get_ns());
698
699 // NOTE: Do NOT modify thread_created flag here!
700 // splash_wait_for_animation() needs to see thread_created==true to know whether to join the thread.
701 // If we set thread_created=false here, splash_wait_for_animation() won't join, and the render loop
702 // will start while the animation thread is still running, causing frame output to conflict with splash.
703 //
704 // Instead, just signal the animation thread to stop via intro_done_time_ns, and let
705 // splash_wait_for_animation() handle the actual thread join.
706
707 // NOTE: DO NOT set is_running = false here! The animation thread sets it to false
708 // when it actually exits (line 633). If we set it to false here, the render loop
709 // guard will think splash is done while the animation thread is still running for
710 // up to 2 more seconds, allowing ASCII frames to render simultaneously with splash.
711 // The is_running flag MUST only be cleared by the animation thread when it finishes.
712
713 return 0;
714}

References atomic_store_u64(), and time_get_ns().

Referenced by display_disable_logging_for_first_frame(), session_client_like_run(), and session_display_write_ascii().

◆ splash_intro_start()

int splash_intro_start ( session_display_ctx_t *  ctx)

#include <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())
  • –no-intro-screen option must not be set
  • Not in snapshot mode (–snapshot)
  • Terminal must be large enough (min 50 cols x 20 rows)

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 639 of file splash.c.

639 {
640 (void)ctx; // Not needed - display context is not actively used
641
642 // Check if splash should display
643 if (!splash_should_display(true)) {
644 return 0;
645 }
646
647 // Check terminal size
648 int width = (int)terminal_get_effective_width();
649 int height = (int)terminal_get_effective_height();
650 if (width < 50 || height < 20) {
651 return 0;
652 }
653
654 // Redirect splash screen to stderr when stdout is piped (not a TTY).
655 // This keeps stdout clean for frame data while splash goes to stderr.
656 // Uses platform_isatty() so non-desktop platforms (iOS, WASM) can override.
657 if (!platform_isatty(STDOUT_FILENO)) {
658 terminal_screen_set_output_fd(STDERR_FILENO);
659 }
660
661 // Initialize log buffer (same pattern as server_status)
663
664 // Clear screen and show cursor
666 (void)terminal_cursor_show();
667
668 // Clear log buffer for clean slate
670
671 // Set running flag
672 atomic_store_bool(&g_splash_state.is_running, true);
673 atomic_store_bool(&g_splash_state.should_stop, false);
674 g_splash_state.frame = 0;
675 g_splash_state.start_time_ns = time_get_ns();
676
677 // Start animation thread (only create once)
678 bool expected_false = false;
679 if (!atomic_cas_bool(&g_splash_state.thread_created, &expected_false, true)) {
680 return 0;
681 }
682
683 int err = asciichat_thread_create(&g_splash_state.anim_thread, "splash_anim", splash_animation_thread, NULL);
684 if (err != ASCIICHAT_OK) {
685 log_warn("Failed to create splash animation thread: error=%d", err);
686 atomic_store_bool(&g_splash_state.thread_created, false);
687 return 0;
688 }
689
690 return 0; // ASCIICHAT_OK
691}
void splash_log_init(void)
Initialize log buffer for splash animation.
Definition splash.c:76
void splash_log_clear(void)
Clear splash log buffer.
Definition splash.c:88
bool splash_should_display(bool is_intro)
Check if splash screen should display.
Definition splash.c:398
void terminal_screen_set_output_fd(int fd)
Set output file descriptor for terminal screens (splash/status)

References ASCIICHAT_OK, asciichat_thread_create, atomic_cas_bool(), atomic_store_bool(), log_warn, platform_isatty(), splash_log_clear(), splash_log_init(), splash_should_display(), terminal_clear_screen(), terminal_cursor_show(), terminal_get_effective_height(), terminal_get_effective_width(), terminal_screen_set_output_fd(), and time_get_ns().

Referenced by session_client_like_run().

◆ splash_is_running()

bool splash_is_running ( void  )

#include <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 725 of file splash.c.

725 {
726 // Check if splash animation is currently rendering
727 // Used to prevent ASCII art from rendering while splash is animating
728 return atomic_load_bool(&g_splash_state.is_running);
729}

References atomic_load_bool().

Referenced by session_client_like_run(), and session_render_loop().

◆ splash_log_append()

void splash_log_append ( const char *  message)

#include <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 <splash.h>

Clear splash log buffer.

Delegates to the standard terminal_screen log clear. Useful when starting fresh splash animation.

Definition at line 88 of file splash.c.

88 {
90}
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 <splash.h>

Destroy splash log buffer.

Delegates to the standard terminal_screen log cleanup.

Definition at line 83 of file splash.c.

83 {
86}
void log_clear_session_log_buffer(void)
Unregister the session log buffer.
Definition log/log.c:1847
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 <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 76 of file splash.c.

76 {
78 if (buf) {
80 }
81}
void log_set_session_log_buffer(session_log_buffer_t *buf)
Register a session log buffer with the logger.
Definition log/log.c:1838
Internal circular buffer structure.
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 <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 <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 716 of file splash.c.

716 {
717 // No-op: stderr suppression is no longer used
718 // Logging works naturally through session_log_buffer
719}

Referenced by session_client_like_run().

◆ splash_set_update_notification()

void splash_set_update_notification ( const char *  notification)

#include <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 776 of file splash.c.

776 {
777 if (lifecycle_init_once(&g_update_notification_lifecycle)) {
778 mutex_init(&g_update_notification_mutex, "update_notification");
779 lifecycle_init_commit(&g_update_notification_lifecycle);
780 }
781 mutex_lock(&g_update_notification_mutex);
782
783 if (!notification || notification[0] == '\0') {
784 g_update_notification[0] = '\0';
785 log_debug("Cleared update notification for splash/status screens");
786 } else {
787 SAFE_STRNCPY(g_update_notification, notification, sizeof(g_update_notification));
788 log_debug("Set update notification for splash/status screens: %s", notification);
789 }
790
791 mutex_unlock(&g_update_notification_mutex);
792}
void lifecycle_init_commit(lifecycle_t *lc)
Definition lifecycle.c:90
bool lifecycle_init_once(lifecycle_t *lc)
Definition lifecycle.c:52

References lifecycle_init_commit(), lifecycle_init_once(), log_debug, mutex_init(), mutex_lock, mutex_unlock, and SAFE_STRNCPY.

◆ splash_should_display()

bool splash_should_display ( bool  is_intro)

#include <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

Definition at line 398 of file splash.c.

398 {
399 // Check option flags (display splash if enabled, regardless of TTY for testing)
400 if (is_intro) {
401 // Allow splash in snapshot mode if loading from URL/file (has loading time)
402 // Skip splash only for quick webcam snapshots (unless explicitly enabled)
403 bool splash_screen_opt = GET_OPTION(splash_screen);
404 bool splash_explicitly_set = GET_OPTION(splash_screen_explicitly_set);
405 bool is_snapshot = GET_OPTION(snapshot_mode);
406 bool has_media = (GET_OPTION(media_url) && strlen(GET_OPTION(media_url)) > 0) ||
407 (GET_OPTION(media_file) && strlen(GET_OPTION(media_file)) > 0);
408
409 // Show splash if enabled, but in snapshot mode only if:
410 // 1. Has media file/URL, OR
411 // 2. Explicitly enabled with --splash-screen=true
412 bool should_display = splash_screen_opt && (!is_snapshot || has_media || splash_explicitly_set);
413 // Show splash if:
414 // 1. Not in snapshot mode, OR
415 // 2. In snapshot mode but loading from URL/file (needs splash during load)
416 return should_display;
417 } else {
418 return GET_OPTION(status_screen);
419 }
420}

References GET_OPTION.

Referenced by splash_intro_start().

◆ splash_wait_for_animation()

void splash_wait_for_animation ( void  )

#include <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
Blocks on the animation thread join
MUST be called before first display_render_frame() call

Definition at line 731 of file splash.c.

731 {
732 // Wait for animation thread to fully exit before rendering ASCII art
733 // This prevents the splash and ASCII art from appearing simultaneously
734 //
735 // The animation thread will exit gracefully when:
736 // - Intro done signal received AND 2 seconds have elapsed, OR
737 // - 30 seconds have elapsed (safety timeout)
738 // - OR a shutdown signal is received
739 //
740 // We block here indefinitely to ensure the splash animation completes before ASCII art starts
741
742 // Only join if we successfully created the thread
743 if (atomic_load_bool(&g_splash_state.thread_created)) {
744 log_dev("[SPLASH_WAIT] Waiting for animation thread to exit...");
745
746 // Check if shutdown was requested - handle it specially
747 bool is_shutting_down = shutdown_is_requested();
748 if (is_shutting_down) {
749 log_dev("[SPLASH_WAIT] Shutdown requested, signaling animation thread to stop");
750 atomic_store_bool(&g_splash_state.should_stop, true);
751 // During shutdown, use a short timeout (100ms)
752 uint64_t timeout_ns = 100LL * NS_PER_MS_INT;
753 (void)asciichat_thread_join_timeout(&g_splash_state.anim_thread, NULL, timeout_ns);
754 } else {
755 // Normal operation: wait indefinitely for the animation thread to finish
756 // This ensures splash animation is 100% done before ASCII art rendering starts
757 asciichat_error_t err = asciichat_thread_join(&g_splash_state.anim_thread, NULL);
758 if (err == ASCIICHAT_OK) {
759 log_dev("[SPLASH_WAIT] Animation thread exited cleanly");
760 } else {
761 log_warn("[SPLASH_WAIT] Animation thread join failed: %s", asciichat_error_string(err));
762 // Force stop the animation thread if join failed
763 atomic_store_bool(&g_splash_state.should_stop, true);
764 // Clear any partial splash output left on screen
766 terminal_cursor_home(STDOUT_FILENO);
767 terminal_flush(STDOUT_FILENO);
768 }
769 }
770
771 // Mark that we've joined (safe to call multiple times - only joins once)
772 atomic_store_bool(&g_splash_state.thread_created, false);
773 }
774}
int asciichat_thread_join_timeout(asciichat_thread_t *thread, void **retval, uint64_t timeout_ns)
Wait for a thread to complete with timeout.
bool shutdown_is_requested(void)
Check if shutdown has been requested.
Definition common.c:73

References ASCIICHAT_OK, asciichat_thread_join, asciichat_thread_join_timeout(), atomic_load_bool(), atomic_store_bool(), log_dev, log_warn, NS_PER_MS_INT, shutdown_is_requested(), terminal_clear_screen(), terminal_cursor_home(), and terminal_flush().

Referenced by session_client_like_run(), and session_render_loop().

◆ update_banner_has_update()

bool update_banner_has_update ( void  )

#include <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 52 of file update_banner.c.

52 {
53 if (!g_update_result_set) {
54 return false;
55 }
56
57 if (lifecycle_init_once(&g_update_result_lifecycle)) {
58 mutex_init(&g_update_result_mutex, "update_banner_result");
59 lifecycle_init_commit(&g_update_result_lifecycle);
60 }
61
62 mutex_lock(&g_update_result_mutex);
63 bool available = g_update_result_set && g_update_result.update_available;
64 mutex_unlock(&g_update_result_mutex);
65 return available;
66}
bool update_available
True if newer version exists.

References lifecycle_init_commit(), lifecycle_init_once(), mutex_init(), mutex_lock, mutex_unlock, and update_check_result_t::update_available.

Referenced by session_client_like_run().

◆ update_banner_print_instructions()

void update_banner_print_instructions ( void  )

#include <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 313 of file update_banner.c.

313 {
314 mutex_lock(&g_update_result_mutex);
316 memcpy(&result, &g_update_result, sizeof(result));
317 mutex_unlock(&g_update_result_mutex);
318
320 char suggestion[512];
321 update_check_get_upgrade_suggestion(method, result.latest_version, suggestion, sizeof(suggestion));
322
324
325 fprintf(stdout, "\nUpdate available: %s → %s\n\n", result.current_version, result.latest_version);
326
327 if (method == INSTALL_METHOD_GITHUB || method == INSTALL_METHOD_UNKNOWN) {
328 fprintf(stdout, "Download the latest release:\n\n %s\n\n", suggestion);
329 } else {
330 fprintf(stdout, "To upgrade, run:\n\n %s\n\n", suggestion);
331 }
332
333 if (result.release_url[0] != '\0') {
334 fprintf(stdout, "Release notes: %s\n\n", result.release_url);
335 }
336
337 fflush(stdout);
338}
install_method_t
Installation method for suggesting upgrade command.
void update_check_get_upgrade_suggestion(install_method_t method, const char *latest_version, char *buffer, size_t buffer_size)
Get upgrade suggestion string.
install_method_t update_check_detect_install_method(void)
Detect installation method.
@ INSTALL_METHOD_UNKNOWN
Unknown installation method.
@ INSTALL_METHOD_GITHUB
Manual install from GitHub releases.
Update check result data.
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.

References update_check_result_t::current_version, INSTALL_METHOD_GITHUB, INSTALL_METHOD_UNKNOWN, update_check_result_t::latest_version, mutex_lock, mutex_unlock, update_check_result_t::release_url, terminal_clear_screen(), update_check_detect_install_method(), and update_check_get_upgrade_suggestion().

Referenced by session_client_like_run().

◆ update_banner_set_result()

void update_banner_set_result ( const update_check_result_t *  result)

#include <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 36 of file update_banner.c.

36 {
37 if (!result || !result->update_available) {
38 return;
39 }
40
41 if (lifecycle_init_once(&g_update_result_lifecycle)) {
42 mutex_init(&g_update_result_mutex, "update_banner_result");
43 lifecycle_init_commit(&g_update_result_lifecycle);
44 }
45
46 mutex_lock(&g_update_result_mutex);
47 memcpy(&g_update_result, result, sizeof(g_update_result));
48 g_update_result_set = true;
49 mutex_unlock(&g_update_result_mutex);
50}

References lifecycle_init_commit(), lifecycle_init_once(), mutex_init(), mutex_lock, mutex_unlock, and update_check_result_t::update_available.

◆ update_banner_show_prompt()

bool update_banner_show_prompt ( struct session_display_ctx *  ctx)

#include <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 166 of file update_banner.c.

166 {
167 if (!ctx) {
168 return false;
169 }
170
171 // Get a copy of the result under mutex
173 mutex_lock(&g_update_result_mutex);
174 memcpy(&result, &g_update_result, sizeof(result));
175 mutex_unlock(&g_update_result_mutex);
176
177 // Get upgrade suggestion
179 char suggestion[512];
180 update_check_get_upgrade_suggestion(method, result.latest_version, suggestion, sizeof(suggestion));
181
182 // Terminal dimensions
183 int term_width = (int)terminal_get_effective_width();
184 int term_height = (int)terminal_get_effective_height();
185
186 // Box sizing
187 int box_width = 52;
188 if (box_width > term_width - 2) {
189 box_width = term_width - 2;
190 }
191 if (box_width < 30) {
192 box_width = 30;
193 }
194
195 int box_height = 16;
196 int start_col = (term_width - box_width) / 2;
197 if (start_col < 0) {
198 start_col = 0;
199 }
200 int start_row = (term_height - box_height) / 2;
201 if (start_row < 0) {
202 start_row = 0;
203 }
204
205 // Build the screen
206 const size_t BUF_SIZE = 4096;
207 char *buffer = SAFE_MALLOC(BUF_SIZE, char *);
208 size_t buf_pos = 0;
209
210#define APPEND(fmt, ...) \
211 do { \
212 int _w = snprintf(buffer + buf_pos, BUF_SIZE - buf_pos, fmt, ##__VA_ARGS__); \
213 if (_w > 0) { \
214 buf_pos += (size_t)_w; \
215 } \
216 } while (0)
217
218 // Clear screen
219 APPEND("\033[2J\033[H");
220
221 int row = 1;
222
223 // Top border
224 append_border(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "╔", "╗");
225
226 // Title (bold yellow)
227 {
228 char title_content[256];
229 snprintf(title_content, sizeof(title_content), "\033[1;33mUpdate Available\033[0m");
230 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, title_content);
231 }
232
233 // Separator
234 append_border(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "╠", "╣");
235
236 // Blank line
237 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "");
238
239 // Current version
240 {
241 char line[256];
242 snprintf(line, sizeof(line), "Current : %s (%.8s)", result.current_version, result.current_sha);
243 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, line);
244 }
245
246 // Latest version (green)
247 {
248 char line[256];
249 snprintf(line, sizeof(line), "Latest : \033[32m%s\033[0m", result.latest_version);
250 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, line);
251 }
252
253 // Exact release source on GitHub.
254 {
255 char line[512];
256 snprintf(line, sizeof(line), "Release : %s", result.release_url);
257 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, line);
258 }
259
260 // Blank line
261 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "");
262
263 // Separator
264 append_border(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "╠", "╣");
265
266 // Upgrade instructions
267 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "To upgrade:");
268 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "");
269
270 // Upgrade command (bold white)
271 {
272 char line[256];
273 snprintf(line, sizeof(line), " \033[1m%s\033[0m", suggestion);
274 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, line);
275 }
276
277 // Blank line
278 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "");
279
280 // Separator
281 append_border(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "╠", "╣");
282
283 // Prompt (grey)
284 {
285 char line[256];
286 snprintf(line, sizeof(line), "\033[90m[Y/Enter] exit to update [N/Esc] continue\033[0m");
287 append_line(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, line);
288 }
289
290 // Bottom border
291 append_border(buffer, &buf_pos, BUF_SIZE, start_row, &row, start_col, box_width, "╚", "╝");
292
293#undef APPEND
294
295 // Write to terminal
296 session_display_write_raw(ctx, buffer, buf_pos);
297 terminal_flush(STDOUT_FILENO);
298 SAFE_FREE(buffer);
299
300 // Block for user input
301 while (true) {
302 keyboard_key_t key = keyboard_read_with_timeout(60000); // 60s timeout, re-loop
303 if (key == 'y' || key == 'Y' || key == '\r' || key == '\n') {
304 return true;
305 }
306 if (key == 'n' || key == 'N' || key == KEY_ESCAPE) {
307 return false;
308 }
309 // Ignore other keys (including KEY_NONE on timeout — just re-render/wait)
310 }
311}
keyboard_key_t keyboard_read_with_timeout(uint32_t timeout_ms)
Read keyboard input with timeout.
char current_sha[41]
Current commit SHA.
#define APPEND(fmt,...)

References APPEND, update_check_result_t::current_sha, update_check_result_t::current_version, KEY_ESCAPE, keyboard_read_with_timeout(), update_check_result_t::latest_version, mutex_lock, mutex_unlock, update_check_result_t::release_url, SAFE_FREE, SAFE_MALLOC, session_display_write_raw(), terminal_flush(), terminal_get_effective_height(), terminal_get_effective_width(), update_check_detect_install_method(), and update_check_get_upgrade_suggestion().

Referenced by session_client_like_run().

◆ update_banner_start_check()

void update_banner_start_check ( void  )

#include <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 358 of file update_banner.c.

358 {
359 if (asciichat_thread_create(&g_update_thread, "update_check", update_check_thread_func, NULL) == 0) {
360 g_update_thread_started = true;
361 }
362}

References asciichat_thread_create.

Referenced by main().

◆ update_banner_wait_for_check()

void update_banner_wait_for_check ( void  )

#include <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 364 of file update_banner.c.

364 {
365 if (!g_update_thread_started) {
366 return;
367 }
368 int ret = asciichat_thread_join_timeout(&g_update_thread, NULL, 5LL * NS_PER_SEC_INT);
369 if (ret != 0) {
370 // Timeout — thread is still alive. Do a blocking join to avoid orphaning it.
371 // The thread has bounded runtime (DNS + HTTP both have timeouts) so this won't hang.
372 log_warn("Update check thread did not finish within 5s, waiting for completion");
373 asciichat_thread_join(&g_update_thread, NULL);
374 }
375 g_update_thread_started = false;
376}

References asciichat_thread_join, asciichat_thread_join_timeout(), log_warn, and NS_PER_SEC_INT.

Referenced by session_client_like_run().