6#include <ascii-chat/media/source.h>
7#include <ascii-chat/media/ffmpeg_decoder.h>
8#include <ascii-chat/media/yt_dlp.h>
9#include <ascii-chat/video/webcam/webcam.h>
10#include <ascii-chat/options/options.h>
11#include <ascii-chat/audio/audio.h>
12#include <ascii-chat/log/logging.h>
13#include <ascii-chat/asciichat_errno.h>
14#include <ascii-chat/platform/abstraction.h>
15#include <ascii-chat/platform/util.h>
16#include <ascii-chat/common/buffer_sizes.h>
17#include <ascii-chat/util/time.h>
65static bool url_has_ffmpeg_native_extension(
const char *url) {
70 const char *question = strchr(url,
'?');
71 size_t url_len = question ? (size_t)(question - url) : strlen(url);
74 const char *dot = NULL;
75 for (
const char *p = url + url_len - 1; p >= url; p--) {
87 const char *ext = dot + 1;
90 if (strcasecmp(ext,
"mp4") == 0 || strcasecmp(ext,
"mkv") == 0 || strcasecmp(ext,
"webm") == 0 ||
91 strcasecmp(ext,
"mov") == 0 || strcasecmp(ext,
"avi") == 0 || strcasecmp(ext,
"flv") == 0 ||
92 strcasecmp(ext,
"ogv") == 0 || strcasecmp(ext,
"ts") == 0 || strcasecmp(ext,
"m2ts") == 0 ||
93 strcasecmp(ext,
"mts") == 0 || strcasecmp(ext,
"3gp") == 0 || strcasecmp(ext,
"3g2") == 0 ||
94 strcasecmp(ext,
"f4v") == 0 || strcasecmp(ext,
"asf") == 0 || strcasecmp(ext,
"wmv") == 0) {
99 if (strcasecmp(ext,
"ogg") == 0 || strcasecmp(ext,
"oga") == 0 || strcasecmp(ext,
"wma") == 0 ||
100 strcasecmp(ext,
"wav") == 0 || strcasecmp(ext,
"flac") == 0 || strcasecmp(ext,
"aac") == 0 ||
101 strcasecmp(ext,
"m4a") == 0 || strcasecmp(ext,
"m4b") == 0 || strcasecmp(ext,
"mp3") == 0 ||
102 strcasecmp(ext,
"aiff") == 0 || strcasecmp(ext,
"au") == 0) {
107 if (strcasecmp(ext,
"m3u8") == 0 || strcasecmp(ext,
"mpd") == 0) {
117static bool url_is_direct_stream(
const char *url) {
122 if (strncmp(url,
"rtsp://", 7) == 0 || strncmp(url,
"rtmp://", 7) == 0) {
127 return url_has_ffmpeg_native_extension(url);
133static asciichat_error_t media_source_resolve_url(
const char *url,
const char *yt_dlp_options,
char *output_url,
134 size_t output_size) {
135 if (!url || !output_url || output_size < 256) {
136 SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for URL resolution");
137 return ERROR_INVALID_PARAM;
140 bool is_direct = url_is_direct_stream(url);
143 log_debug(
"URL is direct stream, passing to FFmpeg directly: %s", url);
144 SAFE_STRNCPY(output_url, url, output_size - 1);
145 output_url[output_size - 1] =
'\0';
149 log_debug(
"URL is complex site, attempting yt-dlp extraction: %s", url);
153 if (yt_dlp_result == ASCIICHAT_OK) {
154 log_debug(
"yt-dlp successfully extracted stream URL");
158 log_error(
"yt-dlp extraction failed for URL: %s", url);
161 log_debug(
"yt-dlp failed, trying FFmpeg as fallback for: %s", url);
162 SAFE_STRNCPY(output_url, url, output_size - 1);
163 output_url[output_size - 1] =
'\0';
164 log_info(
"FFmpeg will attempt to handle URL (yt-dlp extraction failed)");
175 SET_ERRNO(ERROR_MEMORY,
"Failed to allocate media source");
179 memset(source, 0,
sizeof(*source));
186 SET_ERRNO(ERROR_MEMORY,
"Failed to initialize mutex");
193 SET_ERRNO(ERROR_MEMORY,
"Failed to initialize pause mutex");
201 SET_ERRNO(ERROR_MEMORY,
"Failed to initialize seek access mutex");
209 case MEDIA_SOURCE_WEBCAM: {
211 unsigned short int index = 0;
213 int parsed = atoi(path);
214 if (parsed >= 0 && parsed <= USHRT_MAX) {
215 index = (
unsigned short int)parsed;
221 if (webcam_error != ASCIICHAT_OK) {
223 log_error(
"Failed to initialize webcam device %u (error code: %d)", index, webcam_error);
228 if (webcam_error == ERROR_WEBCAM_IN_USE) {
229 SET_ERRNO(ERROR_WEBCAM_IN_USE,
"Webcam device %u is in use", index);
231 SET_ERRNO(ERROR_WEBCAM,
"Failed to initialize webcam device %u", index);
236 log_debug(
"Media source: Webcam device %u", index);
240 case MEDIA_SOURCE_FILE: {
241 if (!path || path[0] ==
'\0') {
242 SET_ERRNO(ERROR_INVALID_PARAM,
"File path is required for FILE source");
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);
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);
259 effective_path = resolved_url;
260 log_debug(
"Using resolved URL for playback");
265 log_warn(
"Failed to cache original URL");
271 SET_ERRNO(ERROR_MEMORY,
"Failed to duplicate file path");
281 log_error(
"Failed to open media file for video: %s", effective_path);
291 if (prefetch_err != ASCIICHAT_OK) {
292 log_error(
"Failed to start video prefetch thread: %s", asciichat_error_string(prefetch_err));
298 log_error(
"Failed to open media file for audio: %s", effective_path);
308 log_debug(
"Media source: URL resolved to stream (separate video/audio decoders)");
312 case MEDIA_SOURCE_STDIN: {
316 log_error(
"Failed to open stdin for video input");
323 if (prefetch_err != ASCIICHAT_OK) {
324 log_error(
"Failed to start stdin video prefetch thread: %s", asciichat_error_string(prefetch_err));
330 log_error(
"Failed to open stdin for audio input");
337 log_debug(
"Media source: stdin (separate video/audio decoders)");
341 case MEDIA_SOURCE_TEST: {
347 log_debug(
"Media source: Test pattern");
352 SET_ERRNO(ERROR_INVALID_PARAM,
"Unknown media source type: %d", type);
423 switch (source->
type) {
424 case MEDIA_SOURCE_WEBCAM:
431 case MEDIA_SOURCE_TEST:
435 case MEDIA_SOURCE_FILE:
436 case MEDIA_SOURCE_STDIN: {
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;
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;
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);
482 log_debug(
"End of file reached, rewinding for loop");
508 switch (source->
type) {
509 case MEDIA_SOURCE_WEBCAM:
510 case MEDIA_SOURCE_TEST:
513 case MEDIA_SOURCE_FILE:
514 case MEDIA_SOURCE_STDIN:
527 if (!source || !buffer || num_samples == 0) {
531 static uint64_t call_count = 0;
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);
544 memset(buffer, 0, num_samples *
sizeof(
float));
548 switch (source->
type) {
549 case MEDIA_SOURCE_WEBCAM:
550 case MEDIA_SOURCE_TEST:
554 case MEDIA_SOURCE_FILE:
555 case MEDIA_SOURCE_STDIN: {
557 log_warn(
"media_source_read_audio: audio_decoder is NULL!");
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);
577 if (audio_pos_after_read >= 0) {
578 last_audio_pos = audio_pos_after_read;
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);
587 log_debug(
"End of file reached (audio), rewinding for loop");
616 switch (source->
type) {
617 case MEDIA_SOURCE_WEBCAM:
618 case MEDIA_SOURCE_TEST:
621 case MEDIA_SOURCE_FILE:
622 case MEDIA_SOURCE_STDIN:
641 if (loop && source->
type == MEDIA_SOURCE_STDIN) {
642 log_warn(
"Loop mode not supported for stdin input (cannot seek)");
651 switch (source->
type) {
652 case MEDIA_SOURCE_WEBCAM:
653 case MEDIA_SOURCE_TEST:
656 case MEDIA_SOURCE_FILE:
657 case MEDIA_SOURCE_STDIN:
674 return ERROR_INVALID_PARAM;
677 switch (source->
type) {
678 case MEDIA_SOURCE_WEBCAM:
679 case MEDIA_SOURCE_TEST:
682 case MEDIA_SOURCE_FILE:
684 return ERROR_INVALID_PARAM;
694 if (video_result != ASCIICHAT_OK) {
703 asciichat_error_t result = ASCIICHAT_OK;
715 case MEDIA_SOURCE_STDIN:
716 return ERROR_NOT_SUPPORTED;
719 return ERROR_INVALID_PARAM;
729 log_warn(
"DEPRECATED: media_source_sync_audio_to_video() called - this function causes audio playback issues. "
730 "Use natural decode rates instead.");
733 return ERROR_INVALID_PARAM;
737 if (source->
type != MEDIA_SOURCE_FILE && source->
type != MEDIA_SOURCE_STDIN) {
759 SET_ERRNO(ERROR_INVALID_PARAM,
"Media source is NULL");
760 return ERROR_INVALID_PARAM;
763 if (timestamp_sec < 0.0) {
764 SET_ERRNO(ERROR_INVALID_PARAM,
"Timestamp must be >= 0.0");
765 return ERROR_INVALID_PARAM;
769 if (source->
type == MEDIA_SOURCE_WEBCAM || source->
type == MEDIA_SOURCE_TEST) {
774 if (source->
type == MEDIA_SOURCE_STDIN) {
775 SET_ERRNO(ERROR_NOT_SUPPORTED,
"Cannot seek stdin");
776 return ERROR_NOT_SUPPORTED;
779 asciichat_error_t result = ASCIICHAT_OK;
784 audio_context_t *audio_ctx = (audio_context_t *)source->
audio_ctx;
785 if (audio_ctx->playback_buffer) {
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);
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);
811 log_info(
"=== Starting audio seek to %.2f sec ===", timestamp_sec);
814 log_info(
"Audio position before seek: %.2f", audio_pos_before);
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);
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);
835 return source ? source->
type : MEDIA_SOURCE_TEST;
843 switch (source->
type) {
844 case MEDIA_SOURCE_WEBCAM:
845 case MEDIA_SOURCE_TEST:
848 case MEDIA_SOURCE_FILE:
849 case MEDIA_SOURCE_STDIN:
865 switch (source->
type) {
866 case MEDIA_SOURCE_WEBCAM:
867 case MEDIA_SOURCE_TEST:
870 case MEDIA_SOURCE_FILE:
871 case MEDIA_SOURCE_STDIN:
887 switch (source->
type) {
888 case MEDIA_SOURCE_WEBCAM:
889 case MEDIA_SOURCE_TEST:
892 case MEDIA_SOURCE_FILE:
893 case MEDIA_SOURCE_STDIN:
bool ffmpeg_decoder_at_end(ffmpeg_decoder_t *decoder)
double ffmpeg_decoder_get_position(ffmpeg_decoder_t *decoder)
image_t * ffmpeg_decoder_read_video_frame(ffmpeg_decoder_t *decoder)
bool ffmpeg_decoder_has_video(ffmpeg_decoder_t *decoder)
double ffmpeg_decoder_get_video_fps(ffmpeg_decoder_t *decoder)
asciichat_error_t ffmpeg_decoder_rewind(ffmpeg_decoder_t *decoder)
bool ffmpeg_decoder_has_audio(ffmpeg_decoder_t *decoder)
asciichat_error_t ffmpeg_decoder_start_prefetch(ffmpeg_decoder_t *decoder)
Start the background frame prefetching thread.
asciichat_error_t ffmpeg_decoder_seek_to_timestamp(ffmpeg_decoder_t *decoder, double timestamp_sec)
void ffmpeg_decoder_destroy(ffmpeg_decoder_t *decoder)
ffmpeg_decoder_t * ffmpeg_decoder_create(const char *path)
size_t ffmpeg_decoder_read_audio_samples(ffmpeg_decoder_t *decoder, float *buffer, size_t num_samples)
ffmpeg_decoder_t * ffmpeg_decoder_create_stdin(void)
double ffmpeg_decoder_get_duration(ffmpeg_decoder_t *decoder)
void audio_ring_buffer_clear(audio_ring_buffer_t *rb)
bool media_source_at_end(media_source_t *source)
bool media_source_is_paused(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)
double media_source_get_position(media_source_t *source)
void media_source_destroy(media_source_t *source)
void media_source_resume(media_source_t *source)
double media_source_get_video_fps(media_source_t *source)
double media_source_get_duration(media_source_t *source)
void media_source_pause(media_source_t *source)
image_t * media_source_read_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)
void media_source_set_audio_context(media_source_t *source, void *audio_ctx)
media_source_t * media_source_create(media_source_type_t type, const char *path)
asciichat_error_t media_source_sync_audio_to_video(media_source_t *source)
void media_source_toggle_pause(media_source_t *source)
asciichat_error_t media_source_rewind(media_source_t *source)
void media_source_set_loop(media_source_t *source, bool loop)
FFmpeg decoder state for video and audio decoding.
int mutex_init(mutex_t *mutex)
int mutex_destroy(mutex_t *mutex)
uint64_t time_get_ns(void)
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index)
image_t * webcam_read(void)
image_t * webcam_read_context(webcam_context_t *ctx)
void webcam_cleanup_context(webcam_context_t *ctx)
asciichat_error_t yt_dlp_extract_stream_url(const char *url, const char *yt_dlp_options, char *output_url, size_t output_size)