ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
source.c File Reference

Unified media source implementation. More...

Go to the source code of this file.

Data Structures

struct  media_source_t
 Media source for video and audio capture. More...
 

Functions

media_source_tmedia_source_create (media_source_type_t type, const char *path)
 
void media_source_destroy (media_source_t *source)
 
image_t * media_source_read_video (media_source_t *source)
 
bool media_source_has_video (media_source_t *source)
 
size_t media_source_read_audio (media_source_t *source, float *buffer, size_t num_samples)
 
bool media_source_has_audio (media_source_t *source)
 
void media_source_set_loop (media_source_t *source, bool loop)
 
bool media_source_at_end (media_source_t *source)
 
asciichat_error_t media_source_rewind (media_source_t *source)
 
asciichat_error_t media_source_sync_audio_to_video (media_source_t *source)
 
asciichat_error_t media_source_seek (media_source_t *source, double timestamp_sec)
 
media_source_type_t media_source_get_type (media_source_t *source)
 
double media_source_get_duration (media_source_t *source)
 
double media_source_get_position (media_source_t *source)
 
double media_source_get_video_fps (media_source_t *source)
 
void media_source_pause (media_source_t *source)
 
void media_source_resume (media_source_t *source)
 
bool media_source_is_paused (media_source_t *source)
 
void media_source_toggle_pause (media_source_t *source)
 
void media_source_set_audio_context (media_source_t *source, void *audio_ctx)
 

Detailed Description

Unified media source implementation.

Definition in file source.c.

Function Documentation

◆ media_source_at_end()

bool media_source_at_end ( media_source_t source)

Definition at line 646 of file source.c.

646 {
647 if (!source) {
648 return true;
649 }
650
651 switch (source->type) {
652 case MEDIA_SOURCE_WEBCAM:
653 case MEDIA_SOURCE_TEST:
654 return false; // Webcam never ends
655
656 case MEDIA_SOURCE_FILE:
657 case MEDIA_SOURCE_STDIN:
658 if (!source->video_decoder) {
659 return true;
660 }
661 // If loop is enabled, we never truly reach end
662 if (source->loop_enabled && source->type == MEDIA_SOURCE_FILE) {
663 return false;
664 }
665 return ffmpeg_decoder_at_end(source->video_decoder);
666
667 default:
668 return true;
669 }
670}
bool ffmpeg_decoder_at_end(ffmpeg_decoder_t *decoder)
media_source_type_t type
Type of media source (webcam, file, stdin, test)
Definition source.c:32
ffmpeg_decoder_t * video_decoder
Video decoder (separate or shared with audio)
Definition source.c:41
bool loop_enabled
Whether to loop playback (for files)
Definition source.c:33

References ffmpeg_decoder_at_end(), media_source_t::loop_enabled, media_source_t::type, and media_source_t::video_decoder.

Referenced by session_capture_at_end().

◆ media_source_create()

media_source_t * media_source_create ( media_source_type_t  type,
const char *  path 
)

Definition at line 172 of file source.c.

172 {
173 media_source_t *source = SAFE_MALLOC(sizeof(media_source_t), media_source_t *);
174 if (!source) {
175 SET_ERRNO(ERROR_MEMORY, "Failed to allocate media source");
176 return NULL;
177 }
178
179 memset(source, 0, sizeof(*source));
180 source->type = type;
181 source->loop_enabled = false;
182 source->is_paused = false;
183
184 // Initialize mutex for protecting shared decoder access (for YouTube URLs)
185 if (mutex_init(&source->decoder_mutex) != 0) {
186 SET_ERRNO(ERROR_MEMORY, "Failed to initialize mutex");
187 SAFE_FREE(source);
188 return NULL;
189 }
190
191 // Initialize mutex for protecting pause state (accessed from keyboard and video threads)
192 if (mutex_init(&source->pause_mutex) != 0) {
193 SET_ERRNO(ERROR_MEMORY, "Failed to initialize pause mutex");
195 SAFE_FREE(source);
196 return NULL;
197 }
198
199 // Initialize mutex for protecting decoder access during seeks (prevents race conditions)
200 if (mutex_init(&source->seek_access_mutex) != 0) {
201 SET_ERRNO(ERROR_MEMORY, "Failed to initialize seek access mutex");
203 mutex_destroy(&source->pause_mutex);
204 SAFE_FREE(source);
205 return NULL;
206 }
207
208 switch (type) {
209 case MEDIA_SOURCE_WEBCAM: {
210 // Parse webcam index from path (if provided)
211 unsigned short int index = 0;
212 if (path) {
213 int parsed = atoi(path);
214 if (parsed >= 0 && parsed <= USHRT_MAX) {
215 index = (unsigned short int)parsed;
216 }
217 }
218
219 source->webcam_index = index;
220 asciichat_error_t webcam_error = webcam_init_context(&source->webcam_ctx, index);
221 if (webcam_error != ASCIICHAT_OK) {
222 // Webcam init failed - log and cleanup
223 log_error("Failed to initialize webcam device %u (error code: %d)", index, webcam_error);
224 SAFE_FREE(source);
225
226 // Explicitly re-set errno to preserve the specific error code for the caller
227 // (log_error or other calls may have cleared the thread-local errno)
228 if (webcam_error == ERROR_WEBCAM_IN_USE) {
229 SET_ERRNO(ERROR_WEBCAM_IN_USE, "Webcam device %u is in use", index);
230 } else {
231 SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam device %u", index);
232 }
233 return NULL;
234 }
235
236 log_debug("Media source: Webcam device %u", index);
237 break;
238 }
239
240 case MEDIA_SOURCE_FILE: {
241 if (!path || path[0] == '\0') {
242 SET_ERRNO(ERROR_INVALID_PARAM, "File path is required for FILE source");
243 SAFE_FREE(source);
244 return NULL;
245 }
246
247 // Resolve URL using smart FFmpeg/yt-dlp routing
248 const char *effective_path = path;
249 char resolved_url[BUFFER_SIZE_XLARGE] = {0};
250 const char *yt_dlp_options = GET_OPTION(yt_dlp_options);
251
252 log_debug("Resolving URL: %s", path);
253 asciichat_error_t resolve_err = media_source_resolve_url(path, yt_dlp_options, resolved_url, sizeof(resolved_url));
254 if (resolve_err != ASCIICHAT_OK) {
255 log_debug("Failed to resolve URL (error: %d)", resolve_err);
256 SAFE_FREE(source);
257 return NULL;
258 }
259 effective_path = resolved_url;
260 log_debug("Using resolved URL for playback");
261
262 // Store original URL for potential re-extraction if needed
264 if (!source->original_youtube_url) {
265 log_warn("Failed to cache original URL");
266 }
267
268 // Cache file path for potential reopen on loop
269 source->file_path = platform_strdup(effective_path);
270 if (!source->file_path) {
271 SET_ERRNO(ERROR_MEMORY, "Failed to duplicate file path");
272 SAFE_FREE(source->original_youtube_url);
273 SAFE_FREE(source);
274 return NULL;
275 }
276
277 // Always use separate decoders for video and audio
278 // This allows independent read rates and avoids lock contention
279 source->video_decoder = ffmpeg_decoder_create(effective_path);
280 if (!source->video_decoder) {
281 log_error("Failed to open media file for video: %s", effective_path);
282 SAFE_FREE(source->file_path);
283 SAFE_FREE(source->original_youtube_url);
284 SAFE_FREE(source);
285 return NULL;
286 }
287
288 // Start prefetch thread for video frames (critical for HTTP performance)
289 // This thread continuously reads frames into a buffer so the render loop never blocks
290 asciichat_error_t prefetch_err = ffmpeg_decoder_start_prefetch(source->video_decoder);
291 if (prefetch_err != ASCIICHAT_OK) {
292 log_error("Failed to start video prefetch thread: %s", asciichat_error_string(prefetch_err));
293 // Don't fail on prefetch error - continue with frame skipping as fallback
294 }
295
296 source->audio_decoder = ffmpeg_decoder_create(effective_path);
297 if (!source->audio_decoder) {
298 log_error("Failed to open media file for audio: %s", effective_path);
300 source->video_decoder = NULL;
301 SAFE_FREE(source->file_path);
302 SAFE_FREE(source->original_youtube_url);
303 SAFE_FREE(source);
304 return NULL;
305 }
306 source->is_shared_decoder = false;
307
308 log_debug("Media source: URL resolved to stream (separate video/audio decoders)");
309 break;
310 }
311
312 case MEDIA_SOURCE_STDIN: {
313 // Create separate decoders for video and audio from stdin
315 if (!source->video_decoder) {
316 log_error("Failed to open stdin for video input");
317 SAFE_FREE(source);
318 return NULL;
319 }
320
321 // Start prefetch thread for stdin video frames
322 asciichat_error_t prefetch_err = ffmpeg_decoder_start_prefetch(source->video_decoder);
323 if (prefetch_err != ASCIICHAT_OK) {
324 log_error("Failed to start stdin video prefetch thread: %s", asciichat_error_string(prefetch_err));
325 // Don't fail on prefetch error - continue with frame skipping as fallback
326 }
327
329 if (!source->audio_decoder) {
330 log_error("Failed to open stdin for audio input");
332 source->video_decoder = NULL;
333 SAFE_FREE(source);
334 return NULL;
335 }
336
337 log_debug("Media source: stdin (separate video/audio decoders)");
338 break;
339 }
340
341 case MEDIA_SOURCE_TEST: {
342 // Test pattern doesn't need webcam context - it's handled in webcam_read()
343 // which checks GET_OPTION(test_pattern) and generates a pattern directly
344 source->webcam_index = 0;
345 source->webcam_ctx = NULL; // No context needed for test pattern
346
347 log_debug("Media source: Test pattern");
348 break;
349 }
350
351 default:
352 SET_ERRNO(ERROR_INVALID_PARAM, "Unknown media source type: %d", type);
353 SAFE_FREE(source);
354 return NULL;
355 }
356
357 return source;
358}
asciichat_error_t ffmpeg_decoder_start_prefetch(ffmpeg_decoder_t *decoder)
Start the background frame prefetching thread.
void ffmpeg_decoder_destroy(ffmpeg_decoder_t *decoder)
ffmpeg_decoder_t * ffmpeg_decoder_create(const char *path)
ffmpeg_decoder_t * ffmpeg_decoder_create_stdin(void)
char * platform_strdup(const char *s)
Media source for video and audio capture.
Definition source.c:31
webcam_context_t * webcam_ctx
Webcam context (NULL for non-webcam types)
Definition source.c:37
bool is_shared_decoder
True if both streams share same decoder (YouTube URLs)
Definition source.c:43
char * file_path
File path (for FILE type)
Definition source.c:51
char * original_youtube_url
Original YouTube URL for re-extraction.
Definition source.c:52
mutex_t seek_access_mutex
Protects decoder access during seeks.
Definition source.c:47
mutex_t decoder_mutex
Protects shared decoder access (YouTube URLs)
Definition source.c:46
mutex_t pause_mutex
Protects pause state.
Definition source.c:48
unsigned short int webcam_index
Webcam device index.
Definition source.c:38
ffmpeg_decoder_t * audio_decoder
Audio decoder (separate or shared with video)
Definition source.c:42
bool is_paused
Whether playback is paused.
Definition source.c:34
int mutex_init(mutex_t *mutex)
Definition threading.c:16
int mutex_destroy(mutex_t *mutex)
Definition threading.c:21
asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index)

References media_source_t::audio_decoder, media_source_t::decoder_mutex, ffmpeg_decoder_create(), ffmpeg_decoder_create_stdin(), ffmpeg_decoder_destroy(), ffmpeg_decoder_start_prefetch(), media_source_t::file_path, media_source_t::is_paused, media_source_t::is_shared_decoder, media_source_t::loop_enabled, mutex_destroy(), mutex_init(), media_source_t::original_youtube_url, media_source_t::pause_mutex, platform_strdup(), media_source_t::seek_access_mutex, media_source_t::type, media_source_t::video_decoder, media_source_t::webcam_ctx, media_source_t::webcam_index, and webcam_init_context().

Referenced by session_capture_create(), and session_client_like_run().

◆ media_source_destroy()

void media_source_destroy ( media_source_t source)

Definition at line 360 of file source.c.

360 {
361 if (!source) {
362 return;
363 }
364
365 // Cleanup based on type
366 if (source->webcam_ctx) {
368 source->webcam_ctx = NULL;
369 }
370
371 // For YouTube (shared decoder), only destroy once
372 // For local files (separate decoders), destroy both
373 if (source->video_decoder) {
375 source->video_decoder = NULL;
376 }
377
378 if (source->audio_decoder) {
379 // Don't double-free if audio and video share the same decoder
380 if (!source->is_shared_decoder) {
382 }
383 source->audio_decoder = NULL;
384 }
385
386 if (source->file_path) {
387 free(source->file_path);
388 source->file_path = NULL;
389 }
390
391 if (source->original_youtube_url) {
392 free(source->original_youtube_url);
393 source->original_youtube_url = NULL;
394 }
395
396 // Destroy mutexes
398 mutex_destroy(&source->pause_mutex);
400
401 SAFE_FREE(source);
402}
void webcam_cleanup_context(webcam_context_t *ctx)

References media_source_t::audio_decoder, media_source_t::decoder_mutex, ffmpeg_decoder_destroy(), media_source_t::file_path, media_source_t::is_shared_decoder, mutex_destroy(), media_source_t::original_youtube_url, media_source_t::pause_mutex, media_source_t::seek_access_mutex, media_source_t::video_decoder, webcam_cleanup_context(), and media_source_t::webcam_ctx.

Referenced by session_capture_create(), session_capture_destroy(), and session_client_like_run().

◆ media_source_get_duration()

double media_source_get_duration ( media_source_t source)

Definition at line 838 of file source.c.

838 {
839 if (!source) {
840 return -1.0;
841 }
842
843 switch (source->type) {
844 case MEDIA_SOURCE_WEBCAM:
845 case MEDIA_SOURCE_TEST:
846 return -1.0; // Infinite
847
848 case MEDIA_SOURCE_FILE:
849 case MEDIA_SOURCE_STDIN:
850 if (!source->video_decoder) {
851 return -1.0;
852 }
854
855 default:
856 return -1.0;
857 }
858}
double ffmpeg_decoder_get_duration(ffmpeg_decoder_t *decoder)

References ffmpeg_decoder_get_duration(), media_source_t::type, and media_source_t::video_decoder.

Referenced by session_handle_keyboard_input().

◆ media_source_get_position()

double media_source_get_position ( media_source_t source)

Definition at line 860 of file source.c.

860 {
861 if (!source) {
862 return -1.0;
863 }
864
865 switch (source->type) {
866 case MEDIA_SOURCE_WEBCAM:
867 case MEDIA_SOURCE_TEST:
868 return -1.0; // No position concept
869
870 case MEDIA_SOURCE_FILE:
871 case MEDIA_SOURCE_STDIN:
872 if (!source->video_decoder) {
873 return -1.0;
874 }
876
877 default:
878 return -1.0;
879 }
880}
double ffmpeg_decoder_get_position(ffmpeg_decoder_t *decoder)

References ffmpeg_decoder_get_position(), media_source_t::type, and media_source_t::video_decoder.

Referenced by session_handle_keyboard_input().

◆ media_source_get_type()

media_source_type_t media_source_get_type ( media_source_t source)

Definition at line 834 of file source.c.

834 {
835 return source ? source->type : MEDIA_SOURCE_TEST;
836}

References media_source_t::type.

Referenced by session_handle_keyboard_input().

◆ media_source_get_video_fps()

double media_source_get_video_fps ( media_source_t source)

Definition at line 882 of file source.c.

882 {
883 if (!source) {
884 return 0.0;
885 }
886
887 switch (source->type) {
888 case MEDIA_SOURCE_WEBCAM:
889 case MEDIA_SOURCE_TEST:
890 return 0.0; // Variable rate, no fixed FPS
891
892 case MEDIA_SOURCE_FILE:
893 case MEDIA_SOURCE_STDIN:
894 if (!source->video_decoder) {
895 return 0.0;
896 }
898
899 default:
900 return 0.0;
901 }
902}
double ffmpeg_decoder_get_video_fps(ffmpeg_decoder_t *decoder)

References ffmpeg_decoder_get_video_fps(), media_source_t::type, and media_source_t::video_decoder.

Referenced by session_client_like_run().

◆ media_source_has_audio()

bool media_source_has_audio ( media_source_t source)

Definition at line 611 of file source.c.

611 {
612 if (!source) {
613 return false;
614 }
615
616 switch (source->type) {
617 case MEDIA_SOURCE_WEBCAM:
618 case MEDIA_SOURCE_TEST:
619 return false; // Webcam/test don't have audio
620
621 case MEDIA_SOURCE_FILE:
622 case MEDIA_SOURCE_STDIN:
623 return source->audio_decoder && ffmpeg_decoder_has_audio(source->audio_decoder);
624
625 default:
626 return false;
627 }
628}
bool ffmpeg_decoder_has_audio(ffmpeg_decoder_t *decoder)

References media_source_t::audio_decoder, ffmpeg_decoder_has_audio(), and media_source_t::type.

Referenced by session_capture_create(), and session_client_like_run().

◆ media_source_has_video()

bool media_source_has_video ( media_source_t source)

Definition at line 503 of file source.c.

503 {
504 if (!source) {
505 return false;
506 }
507
508 switch (source->type) {
509 case MEDIA_SOURCE_WEBCAM:
510 case MEDIA_SOURCE_TEST:
511 return true; // Webcam/test always has video
512
513 case MEDIA_SOURCE_FILE:
514 case MEDIA_SOURCE_STDIN:
515 return source->video_decoder && ffmpeg_decoder_has_video(source->video_decoder);
516
517 default:
518 return false;
519 }
520}
bool ffmpeg_decoder_has_video(ffmpeg_decoder_t *decoder)

References ffmpeg_decoder_has_video(), media_source_t::type, and media_source_t::video_decoder.

◆ media_source_is_paused()

bool media_source_is_paused ( media_source_t source)

Definition at line 926 of file source.c.

926 {
927 if (!source) {
928 return false;
929 }
930 mutex_lock(&source->pause_mutex);
931 bool paused = source->is_paused;
932 mutex_unlock(&source->pause_mutex);
933 return paused;
934}

References media_source_t::is_paused, and media_source_t::pause_mutex.

Referenced by session_handle_keyboard_input(), and session_render_loop().

◆ media_source_pause()

void media_source_pause ( media_source_t source)

Definition at line 908 of file source.c.

908 {
909 if (!source) {
910 return;
911 }
912 mutex_lock(&source->pause_mutex);
913 source->is_paused = true;
914 mutex_unlock(&source->pause_mutex);
915}

References media_source_t::is_paused, and media_source_t::pause_mutex.

Referenced by session_capture_read_frame(), and session_render_loop().

◆ media_source_read_audio()

size_t media_source_read_audio ( media_source_t source,
float *  buffer,
size_t  num_samples 
)

Definition at line 526 of file source.c.

526 {
527 if (!source || !buffer || num_samples == 0) {
528 return 0;
529 }
530
531 static uint64_t call_count = 0;
532 call_count++;
533 if (call_count <= 5 || call_count % 1000 == 0) {
534 log_info("media_source_read_audio #%lu: source_type=%d num_samples=%zu", call_count, source->type, num_samples);
535 }
536
537 // Check pause state (thread-safe)
538 mutex_lock(&source->pause_mutex);
539 bool is_paused = source->is_paused;
540 mutex_unlock(&source->pause_mutex);
541
542 // Return silence immediately if paused (maintaining position)
543 if (is_paused) {
544 memset(buffer, 0, num_samples * sizeof(float));
545 return num_samples;
546 }
547
548 switch (source->type) {
549 case MEDIA_SOURCE_WEBCAM:
550 case MEDIA_SOURCE_TEST:
551 // Webcam/test pattern don't provide audio
552 return 0;
553
554 case MEDIA_SOURCE_FILE:
555 case MEDIA_SOURCE_STDIN: {
556 if (!source->audio_decoder) {
557 log_warn("media_source_read_audio: audio_decoder is NULL!");
558 return 0;
559 }
560
561 // Lock seek_access_mutex to prevent audio callback from reading during seek
562 mutex_lock(&source->seek_access_mutex);
563
564 // Lock shared decoder if YouTube URL (protect against concurrent video thread access)
565 if (source->is_shared_decoder) {
566 mutex_lock(&source->decoder_mutex);
567 }
568
569 double audio_pos_before_read = ffmpeg_decoder_get_position(source->audio_decoder);
570 size_t samples_read = ffmpeg_decoder_read_audio_samples(source->audio_decoder, buffer, num_samples);
571 double audio_pos_after_read = ffmpeg_decoder_get_position(source->audio_decoder);
572
573 static double last_audio_pos = 0;
574 if (audio_pos_after_read >= 0 && last_audio_pos >= 0 && audio_pos_after_read < last_audio_pos) {
575 log_warn("AUDIO POSITION WENT BACKWARD: %.2f → %.2f (LOOPING!)", last_audio_pos, audio_pos_after_read);
576 }
577 if (audio_pos_after_read >= 0) {
578 last_audio_pos = audio_pos_after_read;
579 }
580
581 log_info_every(100 * US_PER_MS_INT, "Audio: read %zu samples, pos %.2f → %.2f", samples_read, audio_pos_before_read,
582 audio_pos_after_read);
583
584 // Handle EOF with loop
585 if (samples_read == 0 && ffmpeg_decoder_at_end(source->audio_decoder)) {
586 if (source->loop_enabled && source->type == MEDIA_SOURCE_FILE) {
587 log_debug("End of file reached (audio), rewinding for loop");
588 if (media_source_rewind(source) == ASCIICHAT_OK) {
589 // Try reading again after rewind
590 samples_read = ffmpeg_decoder_read_audio_samples(source->audio_decoder, buffer, num_samples);
591 }
592 }
593 }
594
595 // Unlock shared decoder
596 if (source->is_shared_decoder) {
597 mutex_unlock(&source->decoder_mutex);
598 }
599
600 // Release seek_access_mutex
601 mutex_unlock(&source->seek_access_mutex);
602
603 return samples_read;
604 }
605
606 default:
607 return 0;
608 }
609}
size_t ffmpeg_decoder_read_audio_samples(ffmpeg_decoder_t *decoder, float *buffer, size_t num_samples)
asciichat_error_t media_source_rewind(media_source_t *source)
Definition source.c:672

References media_source_t::audio_decoder, media_source_t::decoder_mutex, ffmpeg_decoder_at_end(), ffmpeg_decoder_get_position(), ffmpeg_decoder_read_audio_samples(), media_source_t::is_paused, media_source_t::is_shared_decoder, media_source_t::loop_enabled, media_source_rewind(), media_source_t::pause_mutex, media_source_t::seek_access_mutex, and media_source_t::type.

Referenced by session_capture_read_audio().

◆ media_source_read_video()

image_t * media_source_read_video ( media_source_t source)

Definition at line 408 of file source.c.

408 {
409 if (!source) {
410 return NULL;
411 }
412
413 // Check pause state (thread-safe)
414 mutex_lock(&source->pause_mutex);
415 bool is_paused = source->is_paused;
416 mutex_unlock(&source->pause_mutex);
417
418 // Return NULL immediately if paused (maintaining position)
419 if (is_paused) {
420 return NULL;
421 }
422
423 switch (source->type) {
424 case MEDIA_SOURCE_WEBCAM:
425 // Read from webcam
426 if (source->webcam_ctx) {
427 return webcam_read_context(source->webcam_ctx);
428 }
429 return NULL;
430
431 case MEDIA_SOURCE_TEST:
432 // Test pattern uses global webcam_read() which checks GET_OPTION(test_pattern)
433 return webcam_read();
434
435 case MEDIA_SOURCE_FILE:
436 case MEDIA_SOURCE_STDIN: {
437 if (!source->video_decoder) {
438 return NULL;
439 }
440
441 // Lock shared decoder if YouTube URL (protect against concurrent audio thread access)
442 if (source->is_shared_decoder) {
443 mutex_lock(&source->decoder_mutex);
444 }
445
446 uint64_t frame_read_start_ns = time_get_ns();
447 image_t *frame = ffmpeg_decoder_read_video_frame(source->video_decoder);
448 uint64_t frame_read_ns = time_elapsed_ns(frame_read_start_ns, time_get_ns());
449
450 // Track frame reading statistics for FPS diagnosis
451 static uint64_t total_attempts = 0;
452 static uint64_t successful_frames = 0;
453 static uint64_t null_frame_count = 0;
454 static uint64_t total_read_time_ns = 0;
455 static uint64_t max_read_time_ns = 0;
456
457 total_attempts++;
458 if (frame) {
459 successful_frames++;
460 total_read_time_ns += frame_read_ns;
461 if (frame_read_ns > max_read_time_ns) {
462 max_read_time_ns = frame_read_ns;
463 }
464
465 // Log statistics every 30 successful frames
466 if (successful_frames % 30 == 0) {
467 double avg_read_ms = (double)total_read_time_ns / (double)successful_frames / NS_PER_MS;
468 double max_read_ms = (double)max_read_time_ns / NS_PER_MS;
469 double null_rate = (double)null_frame_count * 100.0 / (double)total_attempts;
470 log_info_every(3 * US_PER_SEC_INT,
471 "FRAME_STATS[%lu]: avg_read=%.2f ms, max_read=%.2f ms, null_rate=%.1f%% "
472 "(%lu null/%lu attempts)",
473 successful_frames, avg_read_ms, max_read_ms, null_rate, null_frame_count, total_attempts);
474 }
475 } else {
476 null_frame_count++;
477 }
478
479 // Handle EOF with loop
480 if (!frame && ffmpeg_decoder_at_end(source->video_decoder)) {
481 if (source->loop_enabled && source->type == MEDIA_SOURCE_FILE) {
482 log_debug("End of file reached, rewinding for loop");
483 if (media_source_rewind(source) == ASCIICHAT_OK) {
484 // Try reading again after rewind
486 }
487 }
488 }
489
490 // Unlock shared decoder
491 if (source->is_shared_decoder) {
492 mutex_unlock(&source->decoder_mutex);
493 }
494
495 return frame;
496 }
497
498 default:
499 return NULL;
500 }
501}
image_t * ffmpeg_decoder_read_video_frame(ffmpeg_decoder_t *decoder)
uint64_t time_get_ns(void)
Definition util/time.c:48
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Definition util/time.c:90
image_t * webcam_read(void)
image_t * webcam_read_context(webcam_context_t *ctx)

References media_source_t::decoder_mutex, ffmpeg_decoder_at_end(), ffmpeg_decoder_read_video_frame(), media_source_t::is_paused, media_source_t::is_shared_decoder, media_source_t::loop_enabled, media_source_rewind(), media_source_t::pause_mutex, time_elapsed_ns(), time_get_ns(), media_source_t::type, media_source_t::video_decoder, media_source_t::webcam_ctx, webcam_read(), and webcam_read_context().

Referenced by session_capture_read_frame().

◆ media_source_resume()

void media_source_resume ( media_source_t source)

Definition at line 917 of file source.c.

917 {
918 if (!source) {
919 return;
920 }
921 mutex_lock(&source->pause_mutex);
922 source->is_paused = false;
923 mutex_unlock(&source->pause_mutex);
924}

References media_source_t::is_paused, and media_source_t::pause_mutex.

◆ media_source_rewind()

asciichat_error_t media_source_rewind ( media_source_t source)

Definition at line 672 of file source.c.

672 {
673 if (!source) {
674 return ERROR_INVALID_PARAM;
675 }
676
677 switch (source->type) {
678 case MEDIA_SOURCE_WEBCAM:
679 case MEDIA_SOURCE_TEST:
680 return ASCIICHAT_OK; // No-op for webcam
681
682 case MEDIA_SOURCE_FILE:
683 if (!source->video_decoder || !source->audio_decoder) {
684 return ERROR_INVALID_PARAM;
685 }
686
687 // Lock shared decoder if YouTube URL (protect against concurrent thread access)
688 if (source->is_shared_decoder) {
689 mutex_lock(&source->decoder_mutex);
690 }
691
692 // Rewind video decoder
693 asciichat_error_t video_result = ffmpeg_decoder_rewind(source->video_decoder);
694 if (video_result != ASCIICHAT_OK) {
695 if (source->is_shared_decoder) {
696 mutex_unlock(&source->decoder_mutex);
697 }
698 return video_result;
699 }
700
701 // For YouTube (shared decoder), don't rewind audio separately
702 // For local files (separate decoders), rewind audio too
703 asciichat_error_t result = ASCIICHAT_OK;
704 if (!source->is_shared_decoder) {
705 result = ffmpeg_decoder_rewind(source->audio_decoder);
706 }
707
708 // Unlock shared decoder
709 if (source->is_shared_decoder) {
710 mutex_unlock(&source->decoder_mutex);
711 }
712
713 return result;
714
715 case MEDIA_SOURCE_STDIN:
716 return ERROR_NOT_SUPPORTED; // Cannot seek stdin
717
718 default:
719 return ERROR_INVALID_PARAM;
720 }
721}
asciichat_error_t ffmpeg_decoder_rewind(ffmpeg_decoder_t *decoder)

References media_source_t::audio_decoder, media_source_t::decoder_mutex, ffmpeg_decoder_rewind(), media_source_t::is_shared_decoder, media_source_t::type, and media_source_t::video_decoder.

Referenced by media_source_read_audio(), and media_source_read_video().

◆ media_source_seek()

asciichat_error_t media_source_seek ( media_source_t source,
double  timestamp_sec 
)

Definition at line 757 of file source.c.

757 {
758 if (!source) {
759 SET_ERRNO(ERROR_INVALID_PARAM, "Media source is NULL");
760 return ERROR_INVALID_PARAM;
761 }
762
763 if (timestamp_sec < 0.0) {
764 SET_ERRNO(ERROR_INVALID_PARAM, "Timestamp must be >= 0.0");
765 return ERROR_INVALID_PARAM;
766 }
767
768 // WEBCAM and TEST sources: no-op (always return OK)
769 if (source->type == MEDIA_SOURCE_WEBCAM || source->type == MEDIA_SOURCE_TEST) {
770 return ASCIICHAT_OK;
771 }
772
773 // STDIN: not supported
774 if (source->type == MEDIA_SOURCE_STDIN) {
775 SET_ERRNO(ERROR_NOT_SUPPORTED, "Cannot seek stdin");
776 return ERROR_NOT_SUPPORTED;
777 }
778
779 asciichat_error_t result = ASCIICHAT_OK;
780
781 // Clear audio playback buffer BEFORE seeking to prevent old audio from being queued
782 // This ensures fresh audio starts playing immediately after seek completes
783 if (source->audio_ctx) {
784 audio_context_t *audio_ctx = (audio_context_t *)source->audio_ctx;
785 if (audio_ctx->playback_buffer) {
786 audio_ring_buffer_clear(audio_ctx->playback_buffer);
787 }
788 }
789
790 // The seeking_in_progress flag in decoders blocks prefetch thread
791 // seeking_in_progress coordinates with audio callback via condition variable
792
793 uint64_t seek_start_ns = time_get_ns();
794 if (source->video_decoder) {
795 double video_pos_before = ffmpeg_decoder_get_position(source->video_decoder);
796 asciichat_error_t video_err = ffmpeg_decoder_seek_to_timestamp(source->video_decoder, timestamp_sec);
797 double video_pos_after = ffmpeg_decoder_get_position(source->video_decoder);
798 uint64_t video_seek_ns = time_elapsed_ns(seek_start_ns, time_get_ns());
799 if (video_err != ASCIICHAT_OK) {
800 log_warn("Video seek to %.2f failed: error code %d (took %.1fms)", timestamp_sec, video_err,
801 (double)video_seek_ns / NS_PER_MS);
802 result = video_err;
803 } else {
804 log_info("Video SEEK: %.2f → %.2f sec (target %.2f, took %.1fms)", video_pos_before, video_pos_after,
805 timestamp_sec, (double)video_seek_ns / NS_PER_MS);
806 }
807 }
808
809 // Seek audio decoder (if separate from video)
810 if (source->audio_decoder && !source->is_shared_decoder) {
811 log_info("=== Starting audio seek to %.2f sec ===", timestamp_sec);
812 uint64_t audio_seek_start_ns = time_get_ns();
813 double audio_pos_before = ffmpeg_decoder_get_position(source->audio_decoder);
814 log_info("Audio position before seek: %.2f", audio_pos_before);
815 asciichat_error_t audio_err = ffmpeg_decoder_seek_to_timestamp(source->audio_decoder, timestamp_sec);
816 double audio_pos_after = ffmpeg_decoder_get_position(source->audio_decoder);
817 uint64_t audio_seek_ns = time_elapsed_ns(audio_seek_start_ns, time_get_ns());
818 log_info("Audio position after seek: %.2f", audio_pos_after);
819 if (audio_err != ASCIICHAT_OK) {
820 log_warn("Audio seek to %.2f failed: error code %d (took %.1fms)", timestamp_sec, audio_err,
821 (double)audio_seek_ns / NS_PER_MS);
822 result = audio_err;
823 } else {
824 log_info("Audio SEEK COMPLETE: %.2f → %.2f sec (target %.2f, took %.1fms)", audio_pos_before, audio_pos_after,
825 timestamp_sec, (double)audio_seek_ns / NS_PER_MS);
826 }
827 }
828
829 // Prefetch thread automatically resumes when seeking_in_progress is cleared and signaled
830
831 return result;
832}
asciichat_error_t ffmpeg_decoder_seek_to_timestamp(ffmpeg_decoder_t *decoder, double timestamp_sec)
void audio_ring_buffer_clear(audio_ring_buffer_t *rb)
void * audio_ctx
Audio context for clearing buffers on seek (opaque)
Definition source.c:55

References media_source_t::audio_ctx, media_source_t::audio_decoder, audio_ring_buffer_clear(), ffmpeg_decoder_get_position(), ffmpeg_decoder_seek_to_timestamp(), media_source_t::is_shared_decoder, time_elapsed_ns(), time_get_ns(), media_source_t::type, and media_source_t::video_decoder.

Referenced by session_capture_create(), and session_handle_keyboard_input().

◆ media_source_set_audio_context()

void media_source_set_audio_context ( media_source_t source,
void *  audio_ctx 
)

Definition at line 945 of file source.c.

945 {
946 if (source) {
947 source->audio_ctx = audio_ctx;
948 }
949}

References media_source_t::audio_ctx.

Referenced by session_client_like_run().

◆ media_source_set_loop()

void media_source_set_loop ( media_source_t source,
bool  loop 
)

Definition at line 634 of file source.c.

634 {
635 if (!source) {
636 return;
637 }
638
639 source->loop_enabled = loop;
640
641 if (loop && source->type == MEDIA_SOURCE_STDIN) {
642 log_warn("Loop mode not supported for stdin input (cannot seek)");
643 }
644}

References media_source_t::loop_enabled, and media_source_t::type.

Referenced by session_capture_create().

◆ media_source_sync_audio_to_video()

asciichat_error_t media_source_sync_audio_to_video ( media_source_t source)

Definition at line 723 of file source.c.

723 {
724 // DEPRECATED: This function is deprecated and causes audio playback issues.
725 // Seeking the audio decoder to match video position every ~1 second causes
726 // audio skips and loops. Audio and video naturally stay synchronized when
727 // decoding independently from the same source.
728
729 log_warn("DEPRECATED: media_source_sync_audio_to_video() called - this function causes audio playback issues. "
730 "Use natural decode rates instead.");
731
732 if (!source) {
733 return ERROR_INVALID_PARAM;
734 }
735
736 // Only applicable to FILE and STDIN types
737 if (source->type != MEDIA_SOURCE_FILE && source->type != MEDIA_SOURCE_STDIN) {
738 return ASCIICHAT_OK; // No-op for WEBCAM/TEST
739 }
740
741 // For shared decoders (YouTube URLs), no sync needed (same decoder for both)
742 if (source->is_shared_decoder) {
743 return ASCIICHAT_OK;
744 }
745
746 // NOTE: The actual sync code is disabled because it causes problems.
747 // Get video decoder's current PTS
748 // double video_pts = ffmpeg_decoder_get_position(source->video_decoder);
749 // If we have a valid PTS, seek audio decoder to that position
750 // if (video_pts >= 0.0) {
751 // return ffmpeg_decoder_seek_to_timestamp(source->audio_decoder, video_pts);
752 // }
753
754 return ASCIICHAT_OK;
755}

References media_source_t::is_shared_decoder, and media_source_t::type.

Referenced by session_capture_sync_audio_to_video().

◆ media_source_toggle_pause()

void media_source_toggle_pause ( media_source_t source)

Definition at line 936 of file source.c.

936 {
937 if (!source) {
938 return;
939 }
940 mutex_lock(&source->pause_mutex);
941 source->is_paused = !source->is_paused;
942 mutex_unlock(&source->pause_mutex);
943}

References media_source_t::is_paused, and media_source_t::pause_mutex.

Referenced by session_handle_keyboard_input().