ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
src/server/render.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define SERVER_RENDER_THREAD_DISPLAY_NAME_BUFFER_SIZE 64
4#define SERVER_RENDER_THREAD_NAME_BUFFER_SIZE 64
5
7#include <ascii-chat/otel.h>
9/* Per-client video conversion and audio mixing. Render workers submit media
10 * to the shared ACIP scheduler and are joined before connection destruction. */
11
12#include <stdio.h>
13#include <string.h>
14#include <time.h>
15#include <errno.h>
16#include <math.h>
17
18#include "render.h"
19#include "connections.h"
20#include "client.h"
21#include "main.h"
22#include "stream.h"
23#include "protocol.h"
24#include <ascii-chat/common.h>
27#include <ascii-chat/options/rcu.h> // For RCU-based options access
28#include <libplatform/compiler.h>
29#include <libplatform/system.h>
30#include <libplatform/terminal.h>
39#include <ascii-chat/util/fps.h>
40
41/* ============================================================================
42 * Cross-Platform Utility Functions
43 * ============================================================================
44 */
45
95
96// Removed interruptible_usleep - using regular platform_sleep_us instead
97// Sleep interruption isn't needed for small delays and isn't truly possible anyway
98
99/* ============================================================================
100 * Per-Client Video Rendering Implementation
101 * ============================================================================
102 */
103
203
205 acip_frame_workspace_t event_workspace = {0};
206 client_info_t *client = (client_info_t *)arg;
207 if (!client) {
208 log_error("NULL client pointer in video render thread");
209 return NULL;
210 }
211
212 // Take snapshot of client ID and socket at start to avoid race conditions
213 char thread_client_id[MAX_CLIENT_ID_LEN];
214 SAFE_STRNCPY(thread_client_id, client->client_id, sizeof(thread_client_id) - 1);
215 bool is_webrtc = acip_transport_get_type(client->transport) == ACIP_TRANSPORT_WEBRTC;
216 (void)is_webrtc; // May be unused in release builds
217
218 log_debug("Video render thread: client_id=%s, webrtc=%d", thread_client_id, is_webrtc);
219
220 log_debug("[VIDEO_RENDER_THREAD_START] ★★★ LOCK STATE at thread entry for client %s", thread_client_id);
221 // debug_sync_print_state(); // Disabled: causes AddressSanitizer stack-use-after-return crash
222
223 // Wait for client to send terminal capabilities before rendering
224 // Without capabilities, convert_composite_to_ascii() will fail
225 int timeout_ms = 5000;
226 int waited_ms = 0;
227 while (!client->has_terminal_caps && !atomic_load_bool(&g_should_exit) && !atomic_load_bool(&client->shutting_down)) {
228 if (waited_ms == 0) {
229 log_debug("Waiting for terminal capabilities from client %s...", thread_client_id);
230 }
231 APP_CALLBACK_VOID(platform_pump_events);
233 waited_ms += 10;
234 if (waited_ms >= timeout_ms) {
235 log_debug("Still waiting for terminal capabilities from client %s", thread_client_id);
236 waited_ms = 0;
237 }
238 }
239 if (client->has_terminal_caps) {
240 log_debug("Received terminal capabilities for client %s after %dms", thread_client_id, waited_ms);
241 }
242
243 // Get client's desired FPS from capabilities or use default
244 int client_fps = VIDEO_RENDER_FPS; // Default to 60 FPS
245 // Use snapshot pattern to avoid mutex in render thread
246 bool has_caps = client->has_terminal_caps;
247 int desired_fps = has_caps ? client->terminal_caps.desired_fps : 0;
248 if (has_caps && desired_fps > 0) {
249 client_fps = desired_fps;
250 log_debug("Client %s requested FPS: %d (has_caps=%d, desired_fps=%d)", thread_client_id, client_fps, has_caps,
251 desired_fps);
252 } else {
253 log_debug("Client %s using default FPS: %d (has_caps=%d, desired_fps=%d)", thread_client_id, client_fps, has_caps,
254 desired_fps);
255 }
256
257 int base_frame_interval_ms = 1000 / client_fps;
258 log_debug("Client %s render interval: %dms (%d FPS)", thread_client_id, base_frame_interval_ms, client_fps);
259
260 // FPS tracking for video render thread
261 fps_t video_fps_tracker = {0};
262 fps_init(&video_fps_tracker, client_fps, "SERVER VIDEO");
263
264 const uint64_t frame_interval_ns = NS_PER_SEC_INT / client_fps;
265 uint64_t next_frame_ns = time_get_ns() + frame_interval_ns;
266 bool last_has_sources = false;
267 uint32_t frame_gen_count = 0;
268 uint64_t frame_gen_start_time = 0;
269 uint64_t render_rate_window_start_ns = time_get_ns();
270 uint64_t rendered_frames_in_window = 0;
271 uint64_t missed_deadlines_in_window = 0;
272
273 log_debug("Video render loop STARTING for client %s", thread_client_id);
274
275 bool should_continue = true;
276 while (should_continue && !atomic_load_bool(&g_should_exit) && !atomic_load_bool(&client->shutting_down)) {
277 ASCII_OTEL_SCOPE(SERVER_VIDEO);
278 log_dev_every(10 * NS_PER_MS_INT, "Video render loop iteration for client %s", thread_client_id);
279
280 // Check for immediate shutdown
281 if (atomic_load_bool(&g_should_exit)) {
282 log_debug("Video render thread stopping for client %s (g_should_exit)", thread_client_id);
283 break;
284 }
285
286 bool video_running = atomic_load_bool(&client->video_render_thread_running);
287 bool active = atomic_load_bool(&client->active);
288 bool shutting_down = atomic_load_bool(&client->shutting_down);
289
290 should_continue = video_running && active && !shutting_down;
291
292 if (!should_continue) {
293 log_debug("Video render thread stopping for client %s (should_continue=false: video_running=%d, active=%d, "
294 "shutting_down=%d)",
295 thread_client_id, video_running, active, shutting_down);
296 break;
297 }
298
299 // Processing consumes part of the frame interval. Sleep only until the
300 // scheduled deadline, and skip missed deadlines without a catch-up burst.
301 uint64_t iter_start_ns = time_get_ns();
302 if (iter_start_ns < next_frame_ns) {
303 platform_sleep_ns(next_frame_ns - iter_start_ns);
304 }
305 uint64_t after_sleep_ns = time_get_ns();
306 uint64_t elapsed_intervals =
307 after_sleep_ns >= next_frame_ns ? (after_sleep_ns - next_frame_ns) / frame_interval_ns + 1 : 1;
308 if (elapsed_intervals > 1) {
309 missed_deadlines_in_window += elapsed_intervals - 1;
310 }
311 next_frame_ns += elapsed_intervals * frame_interval_ns;
312
313 // Capture timestamp for FPS tracking and frame timestamps
314 uint64_t current_time_ns = time_get_ns();
315
316 // Check thread state again before acquiring locks (client might have been destroyed during sleep).
317 should_continue = atomic_load_bool(&client->video_render_thread_running) && atomic_load_bool(&client->active) &&
318 !atomic_load_bool(&client->shutting_down);
319 if (!should_continue) {
320 break;
321 }
322
323 // Optimization: No mutex needed - all fields are atomic or stable.
324 // client_id: stable string pointer
325 // width/height: stable uint16_t values (set once, never modified)
326 // active: atomic_t - use atomic_load for thread safety
327 const char *client_id_snapshot = client->client_id; // Stable read
328 unsigned short width_snapshot = client->width; // Stable uint16_t
329 unsigned short height_snapshot = client->height; // Stable uint16_t
330 bool active_snapshot = atomic_load_bool(&client->active); // Atomic read
331
332 // Check if client is still active after getting snapshot
333 if (!active_snapshot) {
334 break;
335 }
336
337 // Phase 2 IMPLEMENTED: Generate frame specifically for THIS client using snapshot data
338 size_t frame_size = 0;
339
340 // Check if any clients are sending video
341 bool has_video_sources = any_clients_sending_video();
342
343 // DIAGNOSTIC: Track when video sources become available
344 if (has_video_sources != last_has_sources) {
345 log_debug("Client %s video sources: %s", thread_client_id, has_video_sources ? "AVAILABLE" : "UNAVAILABLE");
346 last_has_sources = has_video_sources;
347 }
348
350 "Video render iteration for client %s: has_video_sources=%d, width=%u, height=%u", thread_client_id,
351 has_video_sources, width_snapshot, height_snapshot);
352
353 // Use default dimensions if client dimensions not received
354 if (width_snapshot == 0 || height_snapshot == 0) {
355 log_dev_every(5 * NS_PER_MS_INT, "Using default dimensions for client %s (client reported: width=%u, height=%u)",
356 thread_client_id, width_snapshot, height_snapshot);
357 // Use 80x25 as standard terminal size fallback for snapshot/test clients
358 width_snapshot = 80;
359 height_snapshot = 25;
360 }
361
362 // Generate frames for all clients with valid dimensions, regardless of whether other clients are sending video.
363 // This ensures WebSocket snapshot clients (receive-only mode) receive frames even when no other clients are
364 // sending video.
365 int sources_count = 0; // Track number of video sources in this frame
366
367 // DIAGNOSTIC: Track every frame generation attempt
368 frame_gen_count++;
369 if (frame_gen_count == 1) {
370 frame_gen_start_time = current_time_ns;
371 }
372
373 // Log every 120 attempts (should be ~2 seconds at 60 Hz)
374 if (frame_gen_count % 120 == 0) {
375 uint64_t elapsed_ns = current_time_ns - frame_gen_start_time;
376 double gen_fps = (120.0 / (elapsed_ns / (double)NS_PER_SEC_INT));
377 log_dev("render loop: client=%s fps=%.1f", thread_client_id, gen_fps);
378 }
379
380 log_dev_every(5 * NS_PER_MS_INT, "About to call create_mixed_ascii_frame_for_client for client %s with dims %ux%u",
381 thread_client_id, width_snapshot, height_snapshot);
382
383 // debug_sync_print_state() is too expensive to call every frame (kills FPS)
384
385 uint64_t frame_create_start_ns = time_get_ns();
386 acip_render_settings_t frame_rendering = {0};
387 char *ascii_frame = create_mixed_ascii_frame_for_client(client_id_snapshot, width_snapshot, height_snapshot, false,
388 &frame_size, NULL, &sources_count, &frame_rendering);
389 uint64_t frame_create_end_ns = time_get_ns();
390 if (sources_count > 0) {
392 frame_create_end_ns - frame_create_start_ns);
395 }
396
397 if (frame_gen_count % 120 == 0) {
398 char sleep_str[TIME_DURATION_BUFFER_SIZE], create_str[TIME_DURATION_BUFFER_SIZE];
399 time_pretty((uint64_t)(after_sleep_ns - iter_start_ns), -1, sleep_str, sizeof(sleep_str));
400 time_pretty((uint64_t)(frame_create_end_ns - frame_create_start_ns), -1, create_str, sizeof(create_str));
401 log_dev("render timing: adaptive_sleep=%s frame_create=%s", sleep_str, create_str);
402 }
403
404 // Always send frames at configured FPS, even if no external video sources yet
405 // This ensures single-client mode (like testing) still gets continuous frames
406 // The frame data (animated background or test pattern) is still valid content
407
408 if (ascii_frame && frame_size > 0) {
409 server_send_video(client, &event_workspace, ascii_frame, frame_size, frame_rendering.width,
410 frame_rendering.height, sources_count, &frame_rendering);
411 fps_frame_ns(&video_fps_tracker, current_time_ns, "frame rendered");
412 rendered_frames_in_window++;
413 uint64_t render_rate_now_ns = time_get_ns();
414 uint64_t render_rate_elapsed_ns = render_rate_now_ns - render_rate_window_start_ns;
415 if (render_rate_elapsed_ns >= NS_PER_SEC_INT) {
416 double render_fps = (double)rendered_frames_in_window * NS_PER_SEC_INT / (double)render_rate_elapsed_ns;
417 if (render_fps < (double)client_fps * 0.9 || missed_deadlines_in_window > 0) {
418 log_warn("Server video render lagging client=%s fps=%.1f target=%d missed_deadlines=%llu", thread_client_id,
419 render_fps, client_fps, (unsigned long long)missed_deadlines_in_window);
420 } else {
421 log_debug("Server video render client=%s fps=%.1f target=%d missed_deadlines=0", thread_client_id, render_fps,
422 client_fps);
423 }
424 render_rate_window_start_ns = render_rate_now_ns;
425 rendered_frames_in_window = 0;
426 missed_deadlines_in_window = 0;
427 }
428 SAFE_FREE(ascii_frame);
429 }
430
431 uint64_t iter_end_ns = time_get_ns();
432 if (frame_gen_count % 120 == 0) {
433 char total_str[TIME_DURATION_BUFFER_SIZE];
434 time_pretty((uint64_t)(iter_end_ns - iter_start_ns), -1, total_str, sizeof(total_str));
435 log_warn_every(US_PER_SEC_INT, " TIMING: TOTAL_ITERATION=%s", total_str);
436 }
437 STOP_TIMER_AND_LOG(dev, 0, "render_iteration", "render_iteration completed (total)");
438 }
439
440#ifdef DEBUG_THREADS
441 log_debug("Video render thread stopped for client %s", thread_client_id);
442#endif
443 acip_frame_workspace_destroy(&event_workspace);
444
445 // Clean up thread-local error context before exit
446
447 return NULL;
448}
449
450/* ============================================================================
451 * Per-Client Audio Rendering Implementation
452 * ============================================================================
453 */
454
560
562 client_info_t *client = (client_info_t *)arg;
563
564 if (!client) {
565 log_error("Invalid client info in audio render thread");
566 return NULL;
567 }
568
569 // Take snapshot of client ID and display name at start to avoid race conditions
570 char thread_client_id[MAX_CLIENT_ID_LEN];
571 SAFE_STRNCPY(thread_client_id, client->client_id, sizeof(thread_client_id) - 1);
572 char thread_display_name[SERVER_RENDER_THREAD_DISPLAY_NAME_BUFFER_SIZE];
573 bool is_webrtc = acip_transport_get_type(client->transport) == ACIP_TRANSPORT_WEBRTC;
574 (void)is_webrtc; // May be unused in release builds
575
576 // LOCK OPTIMIZATION: Only need client_state_mutex, not global rwlock
577 // We already have a stable client pointer
578 mutex_lock(&client->client_state_mutex);
579 SAFE_STRNCPY(thread_display_name, client->display_name, sizeof(thread_display_name));
580 mutex_unlock(&client->client_state_mutex);
581
582#ifdef DEBUG_THREADS
583 log_debug("Audio render thread started for client %s (%s), webrtc=%d", thread_client_id, thread_display_name,
584 is_webrtc);
585#endif
586
587 // Mix buffer: up to OPUS_FRAME_SAMPLES samples for adaptive reading
588 // Normal: 480 samples = 10ms @ 48kHz
589 // Catchup: OPUS_FRAME_SAMPLES samples = 20ms when buffers are filling up
590 float mix_buffer[OPUS_FRAME_SAMPLES * AUDIO_CHANNELS];
591
592 // Opus frame accumulation buffer (OPUS_FRAME_SAMPLES samples = 20ms @ 48kHz)
593 // Opus requires minimum 480 samples, OPUS_FRAME_SAMPLES is optimal for 20ms frames
594 float opus_frame_buffer[(OPUS_FRAME_SAMPLES * AUDIO_CHANNELS)];
595 int opus_frame_accumulated = 0;
596
597 // Create an Opus encoder for the interleaved stereo stream.
598 opus_codec_t *opus_encoder =
600 if (!opus_encoder) {
601 log_error("Failed to create Opus encoder for audio render thread (client %s)", thread_client_id);
602 return NULL;
603 }
604
605 // FPS tracking for audio render thread
606 fps_t audio_fps_tracker = {0};
607 fps_init(&audio_fps_tracker, AUDIO_PACKET_FPS, "SERVER AUDIO");
608
609 // Per-thread counters (NOT static - each thread instance gets its own)
610 int server_audio_frame_count = 0;
611
612 bool should_continue = true;
613 uint64_t audio_deadline = time_get_ns();
614 while (should_continue && !atomic_load_bool(&g_should_exit) && !atomic_load_bool(&client->shutting_down)) {
615 ASCII_OTEL_SCOPE(SERVER_AUDIO);
616 log_debug_every(LOG_RATE_SLOW, "Audio render loop iteration for client %s", thread_client_id);
617
618 // Check for immediate shutdown
619 if (atomic_load_bool(&g_should_exit)) {
620 log_debug("Audio render thread stopping for client %s (g_should_exit)", thread_client_id);
621 break;
622 }
623
624 // Check thread state before acquiring any locks to prevent use-after-destroy.
625 // If we acquire locks after client is being destroyed, we'll crash with SIGSEGV
626 should_continue = (((int)atomic_load_bool(&client->audio_render_thread_running) != 0) &&
627 ((int)atomic_load_bool(&client->active) != 0) && !atomic_load_bool(&client->shutting_down));
628
629 if (!should_continue) {
630 log_debug("Audio render thread stopping for client %s (should_continue=false)", thread_client_id);
631 break;
632 }
633
634 if (!g_audio_mixer) {
635 log_dev_every(10 * NS_PER_MS_INT, "Audio render waiting for mixer (client %s)", thread_client_id);
636 // Check shutdown flag while waiting
637 if (atomic_load_bool(&g_should_exit))
638 break;
639 platform_sleep_ns(10 * NS_PER_MS_INT);
640 continue;
641 }
642
643 // There is no mixed audio to send when this is the only registered
644 // source. Avoid entering the mixer just to discover that exclusion; this
645 // is the common single-client server case.
647 audio_deadline += 20 * NS_PER_MS_INT;
648 uint64_t idle_now = time_get_ns();
649 if (audio_deadline > idle_now)
650 platform_sleep_ns(audio_deadline - idle_now);
651 else
652 audio_deadline = idle_now;
653 continue;
654 }
655
656 const char *client_id_snapshot = client->client_id;
657 if (!atomic_load_bool(&client->active))
658 break;
659
660 // Create mix excluding THIS client's audio using snapshot data
661
662 // Mix one complete Opus frame per iteration. This keeps the source lock at
663 // the packet cadence instead of reacquiring it twice for every 20 ms of
664 // audio, while preserving the exact frame boundary required by the codec.
665 int samples_to_read = OPUS_FRAME_SAMPLES * AUDIO_CHANNELS;
666
667 int samples_mixed = 0;
668 uint32_t client_id_hash = fnv1a_hash_string(client_id_snapshot);
669 uint64_t stats_mix_start = time_get_ns();
670 samples_mixed = mixer_process_excluding_source(g_audio_mixer, mix_buffer, samples_to_read, client_id_hash);
671 stats_duration_record(stats_runtime_scope(), STATS_DURATION_AUDIO_MIX, time_get_ns() - stats_mix_start);
672
673 log_debug_every(LOG_RATE_SLOW, "Audio render for client %s: samples_mixed=%d", client_id_snapshot, samples_mixed);
674
675 // Accumulate all samples (including 0 or partial) until we have a full Opus frame
676 // This maintains continuous stream without silence padding
677
678 int space_available = (OPUS_FRAME_SAMPLES * AUDIO_CHANNELS) - opus_frame_accumulated;
679 int samples_to_copy = (samples_mixed <= space_available) ? samples_mixed : space_available;
680
681 // Only copy if we have samples, otherwise just wait for next frame
682 if (samples_to_copy > 0) {
683 SAFE_MEMCPY(opus_frame_buffer + opus_frame_accumulated,
684 ((OPUS_FRAME_SAMPLES * AUDIO_CHANNELS) - opus_frame_accumulated) * sizeof(float), mix_buffer,
685 samples_to_copy * sizeof(float));
686 opus_frame_accumulated += samples_to_copy;
687 }
688
689 // Only encode and send when we have accumulated a full Opus frame
690 if (opus_frame_accumulated >= (OPUS_FRAME_SAMPLES * AUDIO_CHANNELS)) {
691 // Encode accumulated Opus frame (OPUS_FRAME_SAMPLES samples = 20ms @ 48kHz)
692 uint8_t opus_buffer[AUDIO_OPUS_SERVER_PACKET_CAPACITY]; // Max Opus frame size
693
694 int opus_size = opus_codec_encode(opus_encoder, opus_frame_buffer, OPUS_FRAME_SAMPLES * AUDIO_CHANNELS,
695 opus_buffer, sizeof(opus_buffer));
696
697 // DEBUG: Log mix buffer and encoding results to see audio levels being sent
698 {
699 float peak = 0.0f, rms = 0.0f;
700 for (int i = 0; i < (OPUS_FRAME_SAMPLES * AUDIO_CHANNELS); i++) {
701 float abs_val = fabsf(opus_frame_buffer[i]);
702 if (abs_val > peak)
703 peak = abs_val;
704 rms += opus_frame_buffer[i] * opus_frame_buffer[i];
705 }
706 rms = sqrtf(rms / (OPUS_FRAME_SAMPLES * AUDIO_CHANNELS));
707 // NOTE: server_audio_frame_count is now per-thread (not static), so each client thread has its own counter
708 server_audio_frame_count++;
709 if (server_audio_frame_count <= 5 || server_audio_frame_count % 20 == 0) {
710 // Log first 4 samples to verify they look like valid audio (not NaN/Inf/garbage)
712 "Server audio frame #%d for client %s: samples_mixed=%d, Peak=%.6f, RMS=%.6f, opus_size=%d, "
713 "first4=[%.4f,%.4f,%.4f,%.4f]",
714 server_audio_frame_count, client_id_snapshot, samples_mixed, peak, rms, opus_size,
715 opus_frame_buffer[0], opus_frame_buffer[1], opus_frame_buffer[2], opus_frame_buffer[3]);
716 }
717 }
718
719 // Always reset accumulation buffer after attempting to encode - we've consumed these samples
720 // If we don't reset, new audio samples would be dropped while stale data sits in the buffer
721 opus_frame_accumulated = 0;
722
723 if (opus_size <= 0) {
724 log_error_every(US_PER_SEC_INT, "Failed to encode audio to Opus for client %s: opus_size=%d",
725 client_id_snapshot, opus_size);
726 } else {
727 // Wrap Opus data in proper batch packet format for client parsing:
728 // Header: sample_rate (4B) + frame_duration (4B) + frame_count (4B)
729 // Frame sizes: frame_count * 2B (one uint16 per frame with Opus data size)
730 // Opus data: raw encoded audio
731
732 // Fixed values for Opus encoding (standard 48kHz, OPUS_FRAME_SAMPLES samples per frame = 20ms)
733 const uint32_t sample_rate = AUDIO_SAMPLE_RATE;
734 const uint32_t frame_duration =
735 CLIENT_AUDIO_PIPELINE_FRAME_MS; // milliseconds (OPUS_FRAME_SAMPLES samples / 48kHz = 20ms)
736 const uint32_t frame_count = 1; // one Opus frame per packet
737 const uint16_t frame_size = (uint16_t)opus_size;
738
739 // Calculate total packet size: 16-byte header + frame_sizes + opus data
740 const size_t header_size = 16; // 4 uint32s (sample_rate, frame_duration, frame_count, reserved)
741 const size_t frame_sizes_size = (size_t)frame_count * sizeof(uint16_t);
742 const size_t total_packet_size = header_size + frame_sizes_size + (size_t)opus_size;
743
744 // Allocate buffer for the complete packet
745 uint8_t *packet_buffer = SAFE_MALLOC(total_packet_size, uint8_t *);
746 if (!packet_buffer) {
747 log_error("Failed to allocate Opus batch packet buffer for client %s (size=%zu)", client_id_snapshot,
748 total_packet_size);
749 } else {
750 // Build header in network byte order
751 uint8_t *ptr = packet_buffer;
752
753 // Write sample_rate (4 bytes, network byte order)
754 uint32_t sr_net = htonl(sample_rate);
755 memcpy(ptr, &sr_net, 4);
756 ptr += 4;
757
758 // Write frame_duration (4 bytes, network byte order)
759 uint32_t fd_net = htonl(frame_duration);
760 memcpy(ptr, &fd_net, 4);
761 ptr += 4;
762
763 // Write frame_count (4 bytes, network byte order)
764 uint32_t fc_net = htonl(frame_count);
765 memcpy(ptr, &fc_net, 4);
766 ptr += 4;
767
768 // Write reserved field (4 bytes, all zeros)
769 uint32_t reserved = 0;
770 memcpy(ptr, &reserved, 4);
771 ptr += 4;
772
773 // Write frame sizes (2 bytes per frame, network byte order)
774 for (uint32_t i = 0; i < frame_count; i++) {
775 uint16_t fs_net = htons(frame_size);
776 memcpy(ptr, &fs_net, 2);
777 ptr += 2;
778 }
779
780 // Copy Opus data
781 memcpy(ptr, opus_buffer, (size_t)opus_size);
782
783 // Queue the properly formatted packet
784 asciichat_error_t result = server_send_audio(client, packet_buffer, total_packet_size);
785
786 if (result != ASCIICHAT_OK) {
787 log_debug_every(US_PER_SEC_INT, "Failed to queue Opus audio for client %s", client_id_snapshot);
788 } else {
789 // FPS tracking - audio packet successfully queued (handles lag detection and periodic reporting)
790 fps_frame_ns(&audio_fps_tracker, time_get_ns(), "audio packet queued");
791 }
792
793 // Free the allocated packet buffer after queuing (packet_queue_push_copy copies data)
794 SAFE_FREE(packet_buffer);
795 }
796 }
797 // NOTE: opus_frame_accumulated is already reset at line 928 after encode attempt
798 }
799
800 // Audio mixing rate limiting using adaptive sleep system.
801 // One complete Opus frame is mixed per 20 ms iteration.
802 // Use queue_depth=0 and target_depth=0 for constant-rate audio processing
803 audio_deadline += 20 * NS_PER_MS_INT;
804 uint64_t now = time_get_ns();
805 if (audio_deadline > now)
806 platform_sleep_ns(audio_deadline - now);
807 else if (now - audio_deadline > 100 * NS_PER_MS_INT)
808 audio_deadline = now;
809 }
810
811#ifdef DEBUG_THREADS
812 log_debug("Audio render thread stopped for client %s", thread_client_id);
813#endif
814
815 // Clean up Opus encoder
816 if (opus_encoder) {
817 opus_codec_destroy(opus_encoder);
818 }
819
820 // Clean up thread-local error context before exit
821
822 return NULL;
823}
824
825/* ============================================================================
826 * Thread Lifecycle Management Functions
827 * ============================================================================
828 */
829
899
901 log_debug("client_id=%s", client ? client->client_id : "NULL");
902
903 if (!server_ctx || !client) {
904 log_error("Cannot create render threads: NULL %s", !server_ctx ? "server_ctx" : "client");
905 return -1;
906 }
907
908#ifdef DEBUG_THREADS
909 log_debug("Creating render threads for client %s", client->client_id);
910#endif
911
912 // NOTE: Mutexes are already initialized in add_client() before any threads start
913 // This prevents race conditions where receive thread tries to use uninitialized mutexes
914
915 // Initialize render thread control flags
916 // IMPORTANT: Set to true BEFORE creating thread to avoid race condition
917 // where thread starts and immediately exits because flag is false
918 atomic_store_bool(&client->video_render_thread_running, true);
919 atomic_store_bool(&client->audio_render_thread_running, true);
920
921 // Create video rendering thread (stop_id=2, stop after receive thread)
923 safe_snprintf(thread_name, sizeof(thread_name), "video_render_%s", client->client_id);
924 asciichat_error_t video_result;
925
926 video_result = platform_thread_create(&client->video_render_thread, thread_name, client_video_render_thread, client);
927
928 if (video_result != ASCIICHAT_OK) {
929 // Reset flag since thread creation failed
930 atomic_store_bool(&client->video_render_thread_running, false);
931 atomic_store_bool(&client->audio_render_thread_running, false);
932 // Mutexes will be destroyed by remove_client() which called us
933 return -1;
934 }
935
936 // Create audio rendering thread (stop_id=2, same priority as video)
937 safe_snprintf(thread_name, sizeof(thread_name), "audio_render_%s", client->client_id);
938 asciichat_error_t audio_result;
939
940 audio_result = platform_thread_create(&client->audio_render_thread, thread_name, client_audio_render_thread, client);
941 if (audio_result != ASCIICHAT_OK) {
942 // Clean up video thread (atomic operation, no mutex needed)
943 atomic_store_bool(&client->video_render_thread_running, false);
944 // Reset audio flag since thread creation failed
945 atomic_store_bool(&client->audio_render_thread_running, false);
946 // stop_client_render_threads() will be called by remove_client()
947 // to clean up the video thread we just created
948 // Mutexes will be destroyed by remove_client() which called us
949 return -1;
950 }
951
952#ifdef DEBUG_THREADS
953 log_debug("Created render threads for client %s", client->client_id);
954#endif
955
956 return 0;
957}
958
1041 if (!client)
1042 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing render client");
1043 atomic_store_bool(&client->video_render_thread_running, false);
1044 atomic_store_bool(&client->audio_render_thread_running, false);
1045 if (thread_is_initialized(&client->video_render_thread)) {
1046 asciichat_error_t result = thread_join(&client->video_render_thread, NULL);
1047 if (result != ASCIICHAT_OK)
1048 return result;
1049 thread_init(&client->video_render_thread);
1050 }
1051 if (thread_is_initialized(&client->audio_render_thread)) {
1052 asciichat_error_t result = thread_join(&client->audio_render_thread, NULL);
1053 if (result != ASCIICHAT_OK)
1054 return result;
1055 thread_init(&client->audio_render_thread);
1056 }
1057 return ASCIICHAT_OK;
1058}
Application-level callbacks for library code.
#define APP_CALLBACK_VOID(callback_name)
#define SET_ERRNO(code,...)
#define OPUS_FRAME_SAMPLES
#define CLIENT_AUDIO_PIPELINE_FRAME_MS
#define AUDIO_SAMPLE_RATE
Definition audio/limits.h:4
#define AUDIO_CHANNELS
Definition audio/limits.h:5
#define AUDIO_OPUS_SERVER_PACKET_CAPACITY
⚙️ Common definitions, error codes, macros, and types shared throughout the application
void server_send_video(client_info_t *client, acip_frame_workspace_t *workspace, const char *data, size_t length, uint32_t width, uint32_t height, int sources, const acip_render_settings_t *rendering)
⏱️ FPS tracking utility for monitoring frame throughput across all threads
atomic_t g_should_exit
Global application exit flag (shared across all modes).
Definition globals.c:34
int mixer_process_excluding_source(mixer_t *mixer, float *output, int num_samples, uint32_t exclude_client_id)
Process audio from all sources except one (for per-client output).
Definition mixer.c:689
void opus_codec_destroy(opus_codec_t *codec)
Destroy an Opus codec instance.
Definition opus.c:280
size_t opus_codec_encode(opus_codec_t *codec, const float *samples, int num_samples, uint8_t *out_data, size_t out_size)
Encode audio frame with Opus.
Definition opus.c:133
int mixer_source_count(const mixer_t *mixer)
Definition mixer.c:507
opus_codec_t * opus_codec_create_encoder_channels(opus_application_t application, int sample_rate, int bitrate, int channels)
Definition opus.c:39
@ OPUS_APPLICATION_AUDIO
General audio (optimized for music).
Definition opus.h:86
unsigned short uint16_t
Definition common.h:61
unsigned int uint32_t
Definition common.h:62
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
void fps_frame_ns(fps_t *tracker, uint64_t current_time_ns, const char *context)
Track a frame and detect lag conditions (nanosecond version - PRIMARY).
Definition fps.c:52
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
#define SAFE_MEMCPY(dest, dest_size, src, count)
Definition common.h:483
void fps_init(fps_t *tracker, int expected_fps, const char *name)
Initialize FPS tracker.
Definition fps.c:32
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
#define MAX_CLIENT_ID_LEN
Maximum client ID length - format: "noun.N (transport:port)" e.g., "mountain.0 (tcp:48036)".
#define LOG_RATE_SLOW
Log rate limit: 10 seconds (10,000,000 microseconds).
Definition log_rates.h:35
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_dev(...)
Log a DEV message (most verbose, development only).
Definition log/log.h:581
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
#define STOP_TIMER_AND_LOG(log_level, threshold_ns, timer_name, msg_fmt,...)
Stop a timer and log the result with a custom message.
Definition time.h:626
#define NS_PER_SEC_INT
Definition time.h:169
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as a compact duration with configurable sub-minute precision.
Definition time.c:374
#define TIME_DURATION_BUFFER_SIZE
Definition time.h:40
#define NS_PER_MS_INT
Definition time.h:168
#define US_PER_MS_INT
Definition time.h:172
#define US_PER_SEC_INT
Definition time.h:173
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
struct client_info client_info_t
🔊 Audio Capture and Playback Interface for ascii-chat
ACIP server-side protocol API.
@ ACIP_TRANSPORT_WEBRTC
WebRTC DataChannel (P2P).
void acip_frame_workspace_destroy(acip_frame_workspace_t *workspace)
void stats_counter_add(stats_scope_t *scope, stats_counter_id_t id, uint64_t amount)
void stats_duration_record(stats_scope_t *scope, stats_duration_id_t id, uint64_t elapsed_ns)
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:749
#define log_error_every(interval_us, fmt,...)
Rate-limited ERROR logging.
Definition log/log.h:758
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:755
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:746
🔢 Mathematical Utility Functions
Multi-Source Audio Mixing and Processing System.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
Opus audio codec wrapper for real-time encoding/decoding.
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
stats_scope_t * stats_runtime_scope(void)
Definition runtime.c:80
mixer_t *volatile g_audio_mixer
Global audio mixer instance for multi-client audio processing.
int server_send_audio(const float *samples, int num_samples)
Send audio data to server.
Per-client state management and lifecycle orchestration.
Server packet processing and protocol implementation.
#define SERVER_RENDER_THREAD_DISPLAY_NAME_BUFFER_SIZE
#define SERVER_RENDER_THREAD_NAME_BUFFER_SIZE
void * client_video_render_thread(void *arg)
Interruptible sleep function with platform-specific optimizations.
void * client_audio_render_thread(void *arg)
Main audio rendering thread function for individual clients.
int create_client_render_threads(server_context_t *server_ctx, client_info_t *client)
Create and initialize per-client rendering threads.
asciichat_error_t stop_client_render_threads(client_info_t *client)
Stop and cleanup per-client rendering threads.
Per-client rendering threads with rate limiting.
#define VIDEO_RENDER_FPS
#define AUDIO_PACKET_FPS
bool any_clients_sending_video(void)
Queue ASCII frame for delivery to specific client.
char * create_mixed_ascii_frame_for_client(const char *target_client_id, unsigned short width, unsigned short height, bool wants_stretch, size_t *out_size, bool *out_grid_changed, int *out_sources_count, acip_render_settings_t *out_rendering)
Generate personalized ASCII frame for a specific client.
Multi-client video mixing and ASCII frame generation.
🔤 String Manipulation and Shell Escaping Utilities
Send ASCII frame to client (server → client).
acip render settings keeps the values that move through the subsystem: caps, width,...
terminal_capabilities_t terminal_caps
char display_name[MAX_DISPLAY_NAME_LEN]
platform_thread_t audio_render_thread
platform_thread_t video_render_thread
char client_id[MAX_CLIENT_ID_LEN]
Counters and timestamps used to calculate and report frame rate.
Definition fps.h:53
State for one Opus encoding or decoding direction.
Definition opus.h:105
Server context - encapsulates all server state.
⏱️ High-precision timing utilities using sokol_time.h and uthash
📊 String Formatting Utilities