ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
source.c
Go to the documentation of this file.
1
13#include <ascii-chat/log/log.h>
19#include <stdlib.h>
20#include <string.h>
21#include <errno.h>
22#include <limits.h>
23
24/* ============================================================================
25 * Media Source Structure
26 * ============================================================================ */
27
38 bool is_paused;
39
40 // Webcam context (for WEBCAM and TEST types)
42 unsigned short int webcam_index;
43
44 // FFmpeg decoders (for FILE and STDIN types)
48
49 // Thread synchronization
53
54 // Cached paths
55 char *file_path;
57
58 // Audio integration
59 void *audio_ctx;
60
61 // Test pattern state (for MEDIA_SOURCE_TEST)
63 unsigned int test_frame_counter;
64};
65
66/* ============================================================================
67 * Stream Type Detection
68 * ============================================================================ */
69
73static bool url_has_ffmpeg_native_extension(const char *url) {
74 if (!url)
75 return false;
76
77 // Extract extension from URL (ignore query params)
78 const char *question = strchr(url, '?');
79 size_t url_len = question ? (size_t)(question - url) : strlen(url);
80
81 // Find last dot
82 const char *dot = NULL;
83 for (const char *p = url + url_len - 1; p >= url; p--) {
84 if (*p == '.') {
85 dot = p;
86 break;
87 }
88 if (*p == '/')
89 break;
90 }
91
92 if (!dot)
93 return false;
94
95 const char *ext = dot + 1;
96
97 // Video containers
98 if (strcasecmp(ext, "mp4") == 0 || strcasecmp(ext, "mkv") == 0 || strcasecmp(ext, "webm") == 0 ||
99 strcasecmp(ext, "mov") == 0 || strcasecmp(ext, "avi") == 0 || strcasecmp(ext, "flv") == 0 ||
100 strcasecmp(ext, "ogv") == 0 || strcasecmp(ext, "ts") == 0 || strcasecmp(ext, "m2ts") == 0 ||
101 strcasecmp(ext, "mts") == 0 || strcasecmp(ext, "3gp") == 0 || strcasecmp(ext, "3g2") == 0 ||
102 strcasecmp(ext, "f4v") == 0 || strcasecmp(ext, "asf") == 0 || strcasecmp(ext, "wmv") == 0) {
103 return true;
104 }
105
106 // Audio containers
107 if (strcasecmp(ext, "ogg") == 0 || strcasecmp(ext, "oga") == 0 || strcasecmp(ext, "wma") == 0 ||
108 strcasecmp(ext, "wav") == 0 || strcasecmp(ext, "flac") == 0 || strcasecmp(ext, "aac") == 0 ||
109 strcasecmp(ext, "m4a") == 0 || strcasecmp(ext, "m4b") == 0 || strcasecmp(ext, "mp3") == 0 ||
110 strcasecmp(ext, "aiff") == 0 || strcasecmp(ext, "au") == 0) {
111 return true;
112 }
113
114 // Still and animated image formats handled directly by FFmpeg
115 if (strcasecmp(ext, "png") == 0 || strcasecmp(ext, "jpg") == 0 || strcasecmp(ext, "jpeg") == 0 ||
116 strcasecmp(ext, "gif") == 0 || strcasecmp(ext, "bmp") == 0 || strcasecmp(ext, "webp") == 0 ||
117 strcasecmp(ext, "tif") == 0 || strcasecmp(ext, "tiff") == 0) {
118 return true;
119 }
120
121 // Streaming
122 if (strcasecmp(ext, "m3u8") == 0 || strcasecmp(ext, "mpd") == 0) {
123 return true;
124 }
125
126 return false;
127}
128
132static bool url_is_direct_stream(const char *url) {
133 if (!url)
134 return false;
135
136 // RTSP, RTMP, and TCP are streaming protocols, always direct
137 if (strncmp(url, "rtsp://", 7) == 0 || strncmp(url, "rtmp://", 7) == 0 || strncmp(url, "tcp://", 6) == 0) {
138 return true;
139 }
140
141 // Check for FFmpeg-native file extension
142 return url_has_ffmpeg_native_extension(url);
143}
144
148static asciichat_error_t media_source_resolve_url(const char *url, const char *yt_dlp_options, char *output_url,
149 size_t output_size) {
150 if (!url || !output_url || output_size < 256) {
151 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for URL resolution");
152 return ERROR_INVALID_PARAM;
153 }
154
155 bool is_direct = url_is_direct_stream(url);
156
157 if (is_direct) {
158 log_debug("URL is direct stream, passing to FFmpeg directly: %s", url);
159 SAFE_STRNCPY(output_url, url, output_size - 1);
160 output_url[output_size - 1] = '\0';
161 return ASCIICHAT_OK;
162 }
163
164 log_debug("URL is complex site, attempting yt-dlp extraction: %s", url);
165
166 // Try yt-dlp extraction (for complex sites like YouTube, Twitch, etc.)
167 asciichat_error_t yt_dlp_result = yt_dlp_extract_stream_url(url, yt_dlp_options, output_url, output_size);
168 if (yt_dlp_result == ASCIICHAT_OK) {
169 log_debug("yt-dlp successfully extracted stream URL");
170 return ASCIICHAT_OK;
171 }
172
173 log_error("yt-dlp extraction failed for URL: %s", url);
174
175 // For complex sites, try FFmpeg as last resort
176 log_debug("yt-dlp failed, trying FFmpeg as fallback for: %s", url);
177 SAFE_STRNCPY(output_url, url, output_size - 1);
178 output_url[output_size - 1] = '\0';
179 log_info("FFmpeg will attempt to handle URL (yt-dlp extraction failed)");
180 return ASCIICHAT_OK; // Let FFmpeg try - if it fails, that's ok too
181}
182
183/* ============================================================================
184 * Media Source Lifecycle
185 * ============================================================================ */
186
189 if (!source) {
190 SET_ERRNO(ERROR_MEMORY, "Failed to allocate media source");
191 return NULL;
192 }
193
194 memset(source, 0, sizeof(*source));
195 source->type = type;
196 source->loop_enabled = false;
197 source->is_paused = false;
198
199 // Initialize mutex for protecting shared decoder access (for YouTube URLs)
200 if (mutex_init(&source->decoder_mutex, "media_decoder") != 0) {
201 SET_ERRNO(ERROR_MEMORY, "Failed to initialize mutex");
202 SAFE_FREE(source);
203 return NULL;
204 }
205
206 // Initialize mutex for protecting pause state (accessed from keyboard and video threads)
207 if (mutex_init(&source->pause_mutex, "media_pause") != 0) {
208 SET_ERRNO(ERROR_MEMORY, "Failed to initialize pause mutex");
210 SAFE_FREE(source);
211 return NULL;
212 }
213
214 // Initialize mutex for protecting decoder access during seeks (prevents race conditions)
215 if (mutex_init(&source->seek_access_mutex, "media_seek") != 0) {
216 SET_ERRNO(ERROR_MEMORY, "Failed to initialize seek access mutex");
218 mutex_destroy(&source->pause_mutex);
219 SAFE_FREE(source);
220 return NULL;
221 }
222
223 switch (type) {
224 case MEDIA_SOURCE_WEBCAM: {
225 // Parse webcam index from path (if provided)
226 unsigned short int index = 0;
227 if (path) {
228 char *endptr;
229 errno = 0;
230 long parsed = strtol(path, &endptr, 10);
231 if (*endptr == '\0' && errno == 0 && parsed >= 0 && parsed <= USHRT_MAX) {
232 index = (unsigned short int)parsed;
233 } else if (*endptr != '\0' || errno != 0) {
234 log_error("media_source_create: Invalid webcam index: %s", path);
235 }
236 }
237
238 source->webcam_index = index;
239 asciichat_error_t webcam_error = webcam_init_context(&source->webcam_ctx, index);
240 if (webcam_error != ASCIICHAT_OK) {
241 // Webcam init failed - log and cleanup
242 log_error("Failed to initialize webcam device %u (error code: %d)", index, webcam_error);
243 // Destroy mutexes before freeing source
245 mutex_destroy(&source->pause_mutex);
247 SAFE_FREE(source);
248
249 // Explicitly re-set errno to preserve the specific error code for the caller
250 // (log_error or other calls may have cleared the thread-local errno)
251 if (webcam_error == ERROR_WEBCAM_IN_USE) {
252 SET_ERRNO(ERROR_WEBCAM_IN_USE, "Webcam device %u is in use", index);
253 } else {
254 SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam device %u", index);
255 }
256 return NULL;
257 }
258
259 log_debug("Media source: Webcam device %u", index);
260 break;
261 }
262
263 case MEDIA_SOURCE_FILE: {
264 if (!path || path[0] == '\0') {
265 SET_ERRNO(ERROR_INVALID_PARAM, "File path is required for FILE source");
266 // Destroy mutexes before freeing source
268 mutex_destroy(&source->pause_mutex);
270 SAFE_FREE(source);
271 return NULL;
272 }
273
274 // Resolve URL using smart FFmpeg/yt-dlp routing
275 const char *effective_path = path;
276 char resolved_url[BUFFER_SIZE_XLARGE] = {0};
277 const char *yt_dlp_options = GET_OPTION(yt_dlp_options);
278
279 log_debug("Resolving URL: %s", path);
280 asciichat_error_t resolve_err = media_source_resolve_url(path, yt_dlp_options, resolved_url, sizeof(resolved_url));
281 if (resolve_err != ASCIICHAT_OK) {
282 log_debug("Failed to resolve URL (error: %d)", resolve_err);
283 // Destroy mutexes before freeing source
285 mutex_destroy(&source->pause_mutex);
287 SAFE_FREE(source);
288 return NULL;
289 }
290 effective_path = resolved_url;
291 log_debug("Using resolved URL for playback");
292
293 // Store original URL for potential re-extraction if needed
295 if (!source->original_youtube_url) {
296 log_warn("Failed to cache original URL");
297 }
298
299 // Cache file path for potential reopen on loop
300 source->file_path = platform_strdup(effective_path);
301 if (!source->file_path) {
302 SET_ERRNO(ERROR_MEMORY, "Failed to duplicate file path");
304 // Destroy mutexes before freeing source
306 mutex_destroy(&source->pause_mutex);
308 SAFE_FREE(source);
309 return NULL;
310 }
311
312 // Always use separate decoders for video and audio
313 // This allows independent read rates and avoids lock contention
314 source->video_decoder = ffmpeg_decoder_create(effective_path);
315 if (!source->video_decoder) {
316 log_error("Failed to open media file for video: %s", effective_path);
317 SAFE_FREE(source->file_path);
319 // Destroy mutexes before freeing source
321 mutex_destroy(&source->pause_mutex);
323 SAFE_FREE(source);
324 return NULL;
325 }
326
327 // Start prefetch only when a video stream exists.
328 // This thread continuously reads frames into a buffer so the render loop never blocks
331 : ASCIICHAT_OK;
332 if (prefetch_err != ASCIICHAT_OK) {
333 log_error("Failed to start video prefetch thread: %s", asciichat_error_string(prefetch_err));
334 // Don't fail on prefetch error - continue with frame skipping as fallback
335 }
336
337 source->audio_decoder = ffmpeg_decoder_create(effective_path);
338 if (!source->audio_decoder) {
339 log_error("Failed to open media file for audio: %s", effective_path);
341 source->video_decoder = NULL;
342 SAFE_FREE(source->file_path);
344 // Destroy mutexes before freeing source
346 mutex_destroy(&source->pause_mutex);
348 SAFE_FREE(source);
349 return NULL;
350 }
351 source->is_shared_decoder = false;
352
353 log_debug("Media source: URL resolved to stream (separate video/audio decoders)");
354 break;
355 }
356
357 case MEDIA_SOURCE_STDIN: {
358 // Create separate decoders for video and audio from stdin
360 if (!source->video_decoder) {
361 log_error("Failed to open stdin for video input");
362 // Destroy mutexes before freeing source
364 mutex_destroy(&source->pause_mutex);
366 SAFE_FREE(source);
367 return NULL;
368 }
369
370 // Start prefetch thread for stdin video frames
373 : ASCIICHAT_OK;
374 if (prefetch_err != ASCIICHAT_OK) {
375 log_error("Failed to start stdin video prefetch thread: %s", asciichat_error_string(prefetch_err));
376 // Don't fail on prefetch error - continue with frame skipping as fallback
377 }
378
380 if (!source->audio_decoder) {
381 log_error("Failed to open stdin for audio input");
383 source->video_decoder = NULL;
384 // Destroy mutexes before freeing source
386 mutex_destroy(&source->pause_mutex);
388 SAFE_FREE(source);
389 return NULL;
390 }
391
392 log_debug("Media source: stdin (separate video/audio decoders)");
393 break;
394 }
395
396 case MEDIA_SOURCE_TEST: {
397 // Test pattern state is encapsulated in media_source_t
398 source->webcam_index = 0;
399 source->webcam_ctx = NULL; // No context needed for test pattern
400 source->test_pattern_frame = NULL; // Allocated lazily on first read
401 source->test_frame_counter = 0;
402
403 log_debug("Media source: Test pattern");
404 break;
405 }
406
407 default:
408 SET_ERRNO(ERROR_INVALID_PARAM, "Unknown media source type: %d", type);
409 // Destroy mutexes before freeing source
411 mutex_destroy(&source->pause_mutex);
413 SAFE_FREE(source);
414 return NULL;
415 }
416
417 if ((type == MEDIA_SOURCE_FILE || type == MEDIA_SOURCE_STDIN) && !ffmpeg_decoder_has_video(source->video_decoder) &&
419 source->audio_only = true;
420 }
421
422 /* Register media source with named registry */
423 char media_type_str[64];
424 switch (type) {
426 snprintf(media_type_str, sizeof(media_type_str), "media_source:webcam");
427 break;
429 snprintf(media_type_str, sizeof(media_type_str), "media_source:file");
430 break;
432 snprintf(media_type_str, sizeof(media_type_str), "media_source:test");
433 break;
434 default:
435 snprintf(media_type_str, sizeof(media_type_str), "media_source:unknown");
436 break;
437 }
438 NAMED_REGISTER(source, media_type_str, "media_source_t", "0x%tx", NULL);
439
440 /* Register media source's sync primitives with hierarchical naming */
441 NAMED_REGISTER_MUTEX(&source->decoder_mutex, "decoder_mutex", (uintptr_t)(const void *)(source));
442 NAMED_REGISTER_MUTEX(&source->pause_mutex, "pause_mutex", (uintptr_t)(const void *)(source));
443 NAMED_REGISTER_MUTEX(&source->seek_access_mutex, "seek_access_mutex", (uintptr_t)(const void *)(source));
444
445 return source;
446}
447
449 if (!source) {
450 return;
451 }
452
453 // Cleanup based on type
454 if (source->webcam_ctx) {
456 source->webcam_ctx = NULL;
457 }
458
459 // For YouTube (shared decoder), only destroy once
460 // For local files (separate decoders), destroy both
461 if (source->video_decoder) {
463 source->video_decoder = NULL;
464 }
465
466 if (source->audio_decoder) {
467 // Don't double-free if audio and video share the same decoder
468 if (!source->is_shared_decoder) {
470 }
471 source->audio_decoder = NULL;
472 }
473
474 SAFE_FREE(source->file_path);
476
477 // Clean up test pattern frame
478 if (source->test_pattern_frame) {
480 source->test_pattern_frame = NULL;
481 }
482
483 // Destroy mutexes
485 mutex_destroy(&source->pause_mutex);
487
488 SAFE_FREE(source);
489}
490
491/* ============================================================================
492 * Video Operations
493 * ============================================================================ */
494
495static image_t *read_test_pattern(media_source_t *source) {
496 if (!source->test_pattern_frame) {
497 source->test_pattern_frame = image_new(320, 240);
498 if (!source->test_pattern_frame)
499 return NULL;
500 }
502 return NULL;
503 return source->test_pattern_frame;
504}
505
507 if (!source)
508 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing media source");
509 if (!source->audio_only || source->webcam_ctx || GET_OPTION(test_pattern))
510 return ASCIICHAT_OK;
511 source->webcam_index = GET_OPTION(webcam_index);
513 if (err != ASCIICHAT_OK)
514 return SET_ERRNO(err, "Audio-only media requires webcam %u", source->webcam_index);
515 log_info("Audio-only media: using webcam %u for video", source->webcam_index);
516 return ASCIICHAT_OK;
517}
518
520 return source && (source->type == MEDIA_SOURCE_WEBCAM || (source->audio_only && !GET_OPTION(test_pattern)));
521}
522
524 if (!source) {
525 return NULL;
526 }
527
528 // Check pause state (thread-safe)
529 mutex_lock(&source->pause_mutex);
530 bool is_paused = source->is_paused;
531 mutex_unlock(&source->pause_mutex);
532
533 // Return NULL immediately if paused (maintaining position)
534 if (is_paused) {
535 return NULL;
536 }
537
538 if (source->audio_only && media_source_at_end(source))
539 return NULL;
540 if (source->audio_only && GET_OPTION(test_pattern))
541 return read_test_pattern(source);
542 if (source->audio_only && !source->webcam_ctx) {
544 return NULL;
545 }
546 if (source->webcam_ctx)
547 return webcam_read_context(source->webcam_ctx);
548
549 switch (source->type) {
551 // Read from webcam
552 if (source->webcam_ctx) {
553 return webcam_read_context(source->webcam_ctx);
554 }
555 return NULL;
556
557 case MEDIA_SOURCE_TEST: {
558 return read_test_pattern(source);
559 }
560
562 case MEDIA_SOURCE_STDIN: {
563 if (!source->video_decoder) {
564 return NULL;
565 }
566
567 // Lock shared decoder if YouTube URL (protect against concurrent audio thread access)
568 if (source->is_shared_decoder) {
569 mutex_lock(&source->decoder_mutex);
570 }
571
572 uint64_t frame_read_start_ns = time_get_ns();
574 uint64_t frame_read_ns = time_elapsed_ns(frame_read_start_ns, time_get_ns());
575
576 // Track frame reading statistics for FPS diagnosis
577 static uint64_t total_attempts = 0;
578 static uint64_t successful_frames = 0;
579 static uint64_t null_frame_count = 0;
580 static uint64_t total_read_time_ns = 0;
581 static uint64_t max_read_time_ns = 0;
582
583 total_attempts++;
584 if (frame) {
585 successful_frames++;
586 total_read_time_ns += frame_read_ns;
587 if (frame_read_ns > max_read_time_ns) {
588 max_read_time_ns = frame_read_ns;
589 }
590
591 // Log statistics every 30 successful frames
592 if (successful_frames % 30 == 0) {
593 double avg_read_ms = (double)total_read_time_ns / (double)successful_frames / NS_PER_MS;
594 double max_read_ms = (double)max_read_time_ns / NS_PER_MS;
595 double null_rate = (double)null_frame_count * 100.0 / (double)total_attempts;
597 "FRAME_STATS[%lu]: avg_read=%.2f ms, max_read=%.2f ms, null_rate=%.1f%% "
598 "(%lu null/%lu attempts)",
599 successful_frames, avg_read_ms, max_read_ms, null_rate, null_frame_count, total_attempts);
600 }
601 } else {
602 null_frame_count++;
603 }
604
605 // Handle EOF with loop
606 if (!frame && ffmpeg_decoder_at_end(source->video_decoder)) {
607 if (source->loop_enabled && source->type == MEDIA_SOURCE_FILE) {
608 log_debug("End of file reached, rewinding for loop");
609 if (media_source_rewind(source) == ASCIICHAT_OK) {
610 // Try reading again after rewind
612 }
613 }
614 }
615
616 // Unlock shared decoder
617 if (source->is_shared_decoder) {
618 mutex_unlock(&source->decoder_mutex);
619 }
620
621 return frame;
622 }
623
624 default:
625 return NULL;
626 }
627}
628
630 if (!source) {
631 return false;
632 }
633
634 switch (source->type) {
637 return true; // Webcam/test always has video
638
641 return source->audio_only || (source->video_decoder && ffmpeg_decoder_has_video(source->video_decoder));
642
643 default:
644 return false;
645 }
646}
647
648/* ============================================================================
649 * Audio Operations
650 * ============================================================================ */
651
652size_t media_source_read_audio(media_source_t *source, float *buffer, size_t num_samples) {
653 if (!source || !buffer || num_samples == 0) {
654 return 0;
655 }
656
657 static uint64_t call_count = 0;
658 call_count++;
659 if (call_count <= 5 || call_count % 1000 == 0) {
660 log_info("media_source_read_audio #%lu: source_type=%d num_samples=%zu", call_count, source->type, num_samples);
661 }
662
663 // Check pause state (thread-safe)
664 mutex_lock(&source->pause_mutex);
665 bool is_paused = source->is_paused;
666 mutex_unlock(&source->pause_mutex);
667
668 // Return silence immediately if paused (maintaining position)
669 if (is_paused) {
670 memset(buffer, 0, num_samples * sizeof(float));
671 return num_samples;
672 }
673
674 switch (source->type) {
677 // Webcam/test pattern don't provide audio
678 return 0;
679
681 case MEDIA_SOURCE_STDIN: {
682 if (!source->audio_decoder) {
683 log_warn("media_source_read_audio: audio_decoder is NULL!");
684 return 0;
685 }
686
687 // Lock seek_access_mutex to prevent audio callback from reading during seek
689
690 // Lock shared decoder if YouTube URL (protect against concurrent video thread access)
691 if (source->is_shared_decoder) {
692 mutex_lock(&source->decoder_mutex);
693 }
694
695 double audio_pos_before_read = ffmpeg_decoder_get_position(source->audio_decoder);
696 size_t samples_read = ffmpeg_decoder_read_audio_samples(source->audio_decoder, buffer, num_samples);
697 double audio_pos_after_read = ffmpeg_decoder_get_position(source->audio_decoder);
698
699 static double last_audio_pos = 0;
700 if (audio_pos_after_read >= 0 && last_audio_pos >= 0 && audio_pos_after_read < last_audio_pos) {
701 log_warn("AUDIO POSITION WENT BACKWARD: %.2f → %.2f (LOOPING!)", last_audio_pos, audio_pos_after_read);
702 }
703 if (audio_pos_after_read >= 0) {
704 last_audio_pos = audio_pos_after_read;
705 }
706
707 log_info_every(100 * US_PER_MS_INT, "Audio: read %zu samples, pos %.2f → %.2f", samples_read,
708 audio_pos_before_read, audio_pos_after_read);
709
710 // Handle EOF with loop
711 if (samples_read == 0 && ffmpeg_decoder_at_end(source->audio_decoder)) {
712 if (source->loop_enabled && source->type == MEDIA_SOURCE_FILE) {
713 log_debug("End of file reached (audio), rewinding for loop");
714 if (media_source_rewind(source) == ASCIICHAT_OK) {
715 // Try reading again after rewind
716 samples_read = ffmpeg_decoder_read_audio_samples(source->audio_decoder, buffer, num_samples);
717 }
718 }
719 }
720
721 // Unlock shared decoder
722 if (source->is_shared_decoder) {
723 mutex_unlock(&source->decoder_mutex);
724 }
725
726 // Release seek_access_mutex
728
729 return samples_read;
730 }
731
732 default:
733 return 0;
734 }
735}
736
738 if (!source) {
739 return false;
740 }
741
742 switch (source->type) {
745 return false; // Webcam/test don't have audio
746
749 return source->audio_decoder && ffmpeg_decoder_has_audio(source->audio_decoder);
750
751 default:
752 return false;
753 }
754}
755
756/* ============================================================================
757 * Playback Control
758 * ============================================================================ */
759
760void media_source_set_loop(media_source_t *source, bool loop) {
761 if (!source) {
762 return;
763 }
764
765 source->loop_enabled = loop;
766
767 if (loop && source->type == MEDIA_SOURCE_STDIN) {
768 log_warn("Loop mode not supported for stdin input (cannot seek)");
769 }
770}
771
773 if (!source) {
774 return true;
775 }
776
777 switch (source->type) {
780 return false; // Webcam never ends
781
784 if (!source->video_decoder) {
785 return true;
786 }
787 // If loop is enabled, we never truly reach end
788 if (source->loop_enabled && source->type == MEDIA_SOURCE_FILE) {
789 return false;
790 }
791 if (source->audio_only)
792 return source->audio_decoder && ffmpeg_decoder_at_end(source->audio_decoder);
793 return ffmpeg_decoder_at_end(source->video_decoder);
794
795 default:
796 return true;
797 }
798}
799
801 if (!source) {
802 return ERROR_INVALID_PARAM;
803 }
804
805 switch (source->type) {
808 return ASCIICHAT_OK; // No-op for webcam
809
811 if (!source->video_decoder || !source->audio_decoder) {
812 return ERROR_INVALID_PARAM;
813 }
814
815 // Lock shared decoder if YouTube URL (protect against concurrent thread access)
816 if (source->is_shared_decoder) {
817 mutex_lock(&source->decoder_mutex);
818 }
819
820 // Rewind video decoder
822 if (video_result != ASCIICHAT_OK) {
823 if (source->is_shared_decoder) {
824 mutex_unlock(&source->decoder_mutex);
825 }
826 return video_result;
827 }
828
829 // For YouTube (shared decoder), don't rewind audio separately
830 // For local files (separate decoders), rewind audio too
832 if (!source->is_shared_decoder) {
833 result = ffmpeg_decoder_rewind(source->audio_decoder);
834 }
835
836 // Unlock shared decoder
837 if (source->is_shared_decoder) {
838 mutex_unlock(&source->decoder_mutex);
839 }
840
841 return result;
842
844 return ERROR_NOT_SUPPORTED; // Cannot seek stdin
845
846 default:
847 return ERROR_INVALID_PARAM;
848 }
849}
850
851asciichat_error_t media_source_seek(media_source_t *source, double timestamp_sec) {
852 if (!source) {
853 SET_ERRNO(ERROR_INVALID_PARAM, "Media source is NULL");
854 return ERROR_INVALID_PARAM;
855 }
856
857 if (timestamp_sec < 0.0) {
858 SET_ERRNO(ERROR_INVALID_PARAM, "Timestamp must be >= 0.0");
859 return ERROR_INVALID_PARAM;
860 }
861
862 // WEBCAM and TEST sources: no-op (always return OK)
863 if (source->type == MEDIA_SOURCE_WEBCAM || source->type == MEDIA_SOURCE_TEST) {
864 return ASCIICHAT_OK;
865 }
866
867 // STDIN: not supported
868 if (source->type == MEDIA_SOURCE_STDIN) {
869 SET_ERRNO(ERROR_NOT_SUPPORTED, "Cannot seek stdin");
870 return ERROR_NOT_SUPPORTED;
871 }
872
874
875 // Clear audio playback buffer BEFORE seeking to prevent old audio from being queued
876 // This ensures fresh audio starts playing immediately after seek completes
877 if (source->audio_ctx) {
878 audio_context_t *audio_ctx = (audio_context_t *)source->audio_ctx;
879 if (audio_ctx->playback_buffer) {
881 }
882 }
883
884 // The seeking_in_progress flag in decoders blocks prefetch thread
885 // seeking_in_progress coordinates with audio callback via condition variable
886
887 uint64_t seek_start_ns = time_get_ns();
888 if (source->video_decoder) {
889 double video_pos_before = ffmpeg_decoder_get_position(source->video_decoder);
890 asciichat_error_t video_err = ffmpeg_decoder_seek_to_timestamp(source->video_decoder, timestamp_sec);
891 double video_pos_after = ffmpeg_decoder_get_position(source->video_decoder);
892 uint64_t video_seek_ns = time_elapsed_ns(seek_start_ns, time_get_ns());
893 char video_seek_str[32];
894 time_pretty(video_seek_ns, -1, video_seek_str, sizeof(video_seek_str));
895 if (video_err != ASCIICHAT_OK) {
896 log_warn("Video seek to %.2f failed: error code %d (took %s)", timestamp_sec, video_err, video_seek_str);
897 result = video_err;
898 } else {
899 log_info("Video SEEK: %.2f → %.2f sec (target %.2f, took %s)", video_pos_before, video_pos_after,
900 timestamp_sec, video_seek_str);
901 }
902 }
903
904 // Seek audio decoder (if separate from video)
905 if (source->audio_decoder && !source->is_shared_decoder) {
906 log_info("=== Starting audio seek to %.2f sec ===", timestamp_sec);
907 uint64_t audio_seek_start_ns = time_get_ns();
908 double audio_pos_before = ffmpeg_decoder_get_position(source->audio_decoder);
909 log_info("Audio position before seek: %.2f", audio_pos_before);
910 asciichat_error_t audio_err = ffmpeg_decoder_seek_to_timestamp(source->audio_decoder, timestamp_sec);
911 double audio_pos_after = ffmpeg_decoder_get_position(source->audio_decoder);
912 uint64_t audio_seek_ns = time_elapsed_ns(audio_seek_start_ns, time_get_ns());
913 log_info("Audio position after seek: %.2f", audio_pos_after);
914 char audio_seek_str[32];
915 time_pretty(audio_seek_ns, -1, audio_seek_str, sizeof(audio_seek_str));
916 if (audio_err != ASCIICHAT_OK) {
917 log_warn("Audio seek to %.2f failed: error code %d (took %s)", timestamp_sec, audio_err, audio_seek_str);
918 result = audio_err;
919 } else {
920 log_info("Audio SEEK COMPLETE: %.2f → %.2f sec (target %.2f, took %s)", audio_pos_before, audio_pos_after,
921 timestamp_sec, audio_seek_str);
922 }
923 }
924
925 // Prefetch thread automatically resumes when seeking_in_progress is cleared and signaled
926
927 return result;
928}
929
933
935 if (!source) {
936 return -1.0;
937 }
938
939 switch (source->type) {
942 return -1.0; // Infinite
943
946 if (!source->video_decoder) {
947 return -1.0;
948 }
950
951 default:
952 return -1.0;
953 }
954}
955
957 if (!source) {
958 return -1.0;
959 }
960
961 switch (source->type) {
964 return -1.0; // No position concept
965
968 if (!source->video_decoder) {
969 return -1.0;
970 }
971 return ffmpeg_decoder_get_position(source->audio_only ? source->audio_decoder : source->video_decoder);
972
973 default:
974 return -1.0;
975 }
976}
977
979 if (!source) {
980 return 0.0;
981 }
982
983 switch (source->type) {
986 return 0.0; // Variable rate, no fixed FPS
987
990 if (!source->video_decoder) {
991 return 0.0;
992 }
994
995 default:
996 return 0.0;
997 }
998}
999
1000/* ============================================================================
1001 * Pause/Resume Control
1002 * ============================================================================ */
1003
1005 if (!source) {
1006 return;
1007 }
1008 mutex_lock(&source->pause_mutex);
1009 source->is_paused = true;
1010 mutex_unlock(&source->pause_mutex);
1011}
1012
1014 if (!source) {
1015 return;
1016 }
1017 mutex_lock(&source->pause_mutex);
1018 source->is_paused = false;
1019 mutex_unlock(&source->pause_mutex);
1020}
1021
1023 if (!source) {
1024 return false;
1025 }
1026 mutex_lock(&source->pause_mutex);
1027 bool paused = source->is_paused;
1028 mutex_unlock(&source->pause_mutex);
1029 return paused;
1030}
1031
1033 if (!source) {
1034 return;
1035 }
1036 mutex_lock(&source->pause_mutex);
1037 source->is_paused = !source->is_paused;
1038 mutex_unlock(&source->pause_mutex);
1039}
1040
1041void media_source_set_audio_context(media_source_t *source, void *audio_ctx) {
1042 if (source) {
1043 source->audio_ctx = audio_ctx;
1044 }
1045}
1046
1047void media_source_set_exit_callback(media_source_t *source, bool (*should_exit_callback)(void *), void *user_data) {
1048 if (!source) {
1049 return;
1050 }
1051
1052 // Set exit callback on video decoder if present
1053 if (source->video_decoder) {
1054 ffmpeg_decoder_set_exit_callback(source->video_decoder, should_exit_callback, user_data);
1055 }
1056
1057 // Set exit callback on audio decoder if present (only if different from video decoder)
1058 if (source->audio_decoder && source->audio_decoder != source->video_decoder) {
1059 ffmpeg_decoder_set_exit_callback(source->audio_decoder, should_exit_callback, user_data);
1060 }
1061}
Platform abstraction layer umbrella header providing unified cross-platform API.
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
Common buffer size definitions.
Named object registry for debugging — log identifiable resource names.
Cross-platform error handling utilities.
🎞️ FFmpeg-based media decoder for video and audio streams
void audio_ring_buffer_clear(audio_ring_buffer_t *rb)
Clear all audio samples from ring buffer.
#define BUFFER_SIZE_XLARGE
Extra large buffer size (2048 bytes)
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
unsigned long long uint64_t
Definition common.h:59
#define NAMED_REGISTER(ptr, name, type, fmt, parent_ptr)
Register any pointer with base name, type, format spec, and location (auto-suffix)
#define NAMED_REGISTER_MUTEX(mutex, name, parent_ptr)
Register a mutex with automatic format specifier.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_WEBCAM_IN_USE
Definition error_codes.h:65
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
@ ERROR_WEBCAM
Definition error_codes.h:64
@ ERROR_NOT_SUPPORTED
Definition error_codes.h:74
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
bool ffmpeg_decoder_at_end(ffmpeg_decoder_t *decoder)
Check if decoder reached end of stream.
double ffmpeg_decoder_get_position(ffmpeg_decoder_t *decoder)
Get current playback position in seconds.
bool media_source_at_end(media_source_t *source)
Check if media source reached end of stream.
Definition source.c:772
image_t * ffmpeg_decoder_read_video_frame(ffmpeg_decoder_t *decoder)
Decode next video frame.
bool ffmpeg_decoder_has_video(ffmpeg_decoder_t *decoder)
Check if decoder has video stream.
double ffmpeg_decoder_get_video_fps(ffmpeg_decoder_t *decoder)
Get video frame rate.
bool media_source_is_paused(media_source_t *source)
Check if media source is paused.
Definition source.c:1022
asciichat_error_t ffmpeg_decoder_rewind(ffmpeg_decoder_t *decoder)
Seek to beginning of media.
bool media_source_has_video(media_source_t *source)
Check if media source has video stream.
Definition source.c:629
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
media_source_type_t
Media source type enumeration.
Definition source.h:81
bool media_source_has_audio(media_source_t *source)
Check if media source has audio stream.
Definition source.c:737
double media_source_get_position(media_source_t *source)
Get current playback position in seconds.
Definition source.c:956
void media_source_destroy(media_source_t *source)
Destroy media source and free resources.
Definition source.c:448
void media_source_resume(media_source_t *source)
Resume media playback after pause.
Definition source.c:1013
double media_source_get_video_fps(media_source_t *source)
Get video frame rate in frames per second.
Definition source.c:978
double media_source_get_duration(media_source_t *source)
Get media duration in seconds.
Definition source.c:934
bool ffmpeg_decoder_has_audio(ffmpeg_decoder_t *decoder)
Check if decoder has audio stream.
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
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
asciichat_error_t yt_dlp_extract_stream_url(const char *url, const char *yt_dlp_options, char *output_url, size_t output_size)
Extract stream URL using yt-dlp with optional parameters.
Definition yt_dlp.c:150
media_source_type_t media_source_get_type(media_source_t *source)
Get media source type.
Definition source.c:930
asciichat_error_t ffmpeg_decoder_start_prefetch(ffmpeg_decoder_t *decoder)
Start the background frame prefetching thread.
void media_source_set_audio_context(media_source_t *source, void *audio_ctx)
Set audio context for playback buffer management.
Definition source.c:1041
asciichat_error_t ffmpeg_decoder_seek_to_timestamp(ffmpeg_decoder_t *decoder, double timestamp_sec)
Seek to specific timestamp in media.
media_source_t * media_source_create(media_source_type_t type, const char *path)
Create a new media source.
Definition source.c:187
void ffmpeg_decoder_set_exit_callback(ffmpeg_decoder_t *decoder, bool(*should_exit_callback)(void *), void *user_data)
Set exit signal callback for graceful shutdown during I/O.
void ffmpeg_decoder_destroy(ffmpeg_decoder_t *decoder)
Destroy FFmpeg decoder and free resources.
ffmpeg_decoder_t * ffmpeg_decoder_create(const char *path)
Create FFmpeg decoder from file path.
void media_source_toggle_pause(media_source_t *source)
Toggle pause state of media source.
Definition source.c:1032
asciichat_error_t media_source_rewind(media_source_t *source)
Rewind media source to beginning.
Definition source.c:800
size_t ffmpeg_decoder_read_audio_samples(ffmpeg_decoder_t *decoder, float *buffer, size_t num_samples)
Decode audio samples.
ffmpeg_decoder_t * ffmpeg_decoder_create_stdin(void)
Create FFmpeg decoder from stdin.
double ffmpeg_decoder_get_duration(ffmpeg_decoder_t *decoder)
Get media duration in seconds.
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_WEBCAM
Hardware webcam device.
Definition source.h:82
@ MEDIA_SOURCE_FILE
Media file (video/audio)
Definition source.h:83
@ MEDIA_SOURCE_TEST
Test pattern generator.
Definition source.h:85
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
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 NS_PER_MS
Definition time.h:147
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition util/time.c:150
#define US_PER_MS_INT
Definition time.h:160
#define US_PER_SEC_INT
Definition time.h:161
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
int errno
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
asciichat_error_t image_render_test_pattern(image_t *image, unsigned int *frame_counter)
Render animated color bars and grid lines into an existing image.
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index)
Initialize webcam context for advanced operations.
struct webcam_context_t webcam_context_t
Opaque webcam context structure.
Definition webcam.h:150
image_t * webcam_read_context(webcam_context_t *ctx)
Capture a frame from webcam context.
void webcam_cleanup_context(webcam_context_t *ctx)
Clean up webcam context and release resources.
🔊 Audio Capture and Playback Interface for ascii-chat
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
Application limits and constraints.
📝 Logging API with multiple log levels and terminal output control
#define log_info_every(interval_us, fmt,...)
Rate-limited INFO logging.
Definition log/log.h:705
bool media_source_uses_webcam(media_source_t *source)
Definition source.c:519
asciichat_error_t media_source_start_video(media_source_t *source)
Definition source.c:506
🎬 Unified media source abstraction for webcam, files, and stdin
int frame
Definition splash.c:99
Audio context for full-duplex capture and playback.
audio_ring_buffer_t * playback_buffer
OLD: Network → Worker (will become network_playback_rb)
FFmpeg decoder state for video and audio decoding.
Image structure.
Media source for video and audio capture.
Definition source.c:34
bool audio_only
File audio uses the selected webcam for video.
Definition source.c:37
webcam_context_t * webcam_ctx
Webcam context (NULL for non-webcam types)
Definition source.c:41
bool is_shared_decoder
True if both streams share same decoder (YouTube URLs)
Definition source.c:47
unsigned int test_frame_counter
Animation phase counter for test pattern.
Definition source.c:63
char * file_path
File path (for FILE type)
Definition source.c:55
void * audio_ctx
Audio context for clearing buffers on seek (opaque)
Definition source.c:59
char * original_youtube_url
Original YouTube URL for re-extraction.
Definition source.c:56
media_source_type_t type
Type of media source (webcam, file, stdin, test)
Definition source.c:35
image_t * test_pattern_frame
Reusable 320x240 frame buffer for test pattern.
Definition source.c:62
mutex_t seek_access_mutex
Protects decoder access during seeks.
Definition source.c:51
mutex_t decoder_mutex
Protects shared decoder access (YouTube URLs)
Definition source.c:50
mutex_t pause_mutex
Protects pause state.
Definition source.c:52
unsigned short int webcam_index
Webcam device index.
Definition source.c:42
ffmpeg_decoder_t * audio_decoder
Audio decoder (separate or shared with video)
Definition source.c:46
bool is_paused
Whether playback is paused.
Definition source.c:38
ffmpeg_decoder_t * video_decoder
Video decoder (separate or shared with audio)
Definition source.c:45
bool loop_enabled
Whether to loop playback (for files)
Definition source.c:36
Mutex type (POSIX: pthread_mutex_t with debug tracking)
⏱️ High-precision timing utilities using sokol_time.h and uthash
yt-dlp stream URL extraction