ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
media/ffmpeg_decoder.c
Go to the documentation of this file.
1
7#include <ascii-chat/common.h>
9#include <ascii-chat/log/io.h>
16#include <ascii-chat/util/url.h>
18
19#include <libavformat/avformat.h>
20#include <libavcodec/avcodec.h>
21#include <libavutil/imgutils.h>
22#include <libavutil/opt.h>
23#include <libavutil/log.h>
24#include <libswscale/swscale.h>
25#include <libswresample/swresample.h>
26#include <string.h>
27#include <inttypes.h>
28
29/* ============================================================================
30 * FFmpeg Logging Suppression
31 * ============================================================================ */
32
37static void ffmpeg_silent_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
38 (void)avcl; // unused
39 (void)level; // unused
40 (void)fmt; // unused
41 (void)vl; // unused
42 // Do nothing - silently discard all FFmpeg logs
43}
44
45/* ============================================================================
46 * Constants
47 * ============================================================================ */
48
50#define TARGET_SAMPLE_RATE 48000
51
53#define TARGET_CHANNELS 1
54
56#define AVIO_BUFFER_SIZE (64 * 1024)
57
58/* ============================================================================
59 * FFmpeg Decoder Structure
60 * ============================================================================ */
61
69 // Format context
70 AVFormatContext *format_ctx;
71
72 // Video stream
73 AVCodecContext *video_codec_ctx;
75 struct SwsContext *sws_ctx;
76
77 // Audio stream
78 AVCodecContext *audio_codec_ctx;
80 struct SwrContext *swr_ctx;
81
82 // Frame and packet buffers
83 AVFrame *frame;
84 AVPacket *packet;
85
86 // Decoded image cache - double-buffered for prefetching
88
89 // Audio sample buffer (for partial frame handling)
90 float *audio_buffer;
93
94 // Background thread for frame prefetching (reduces YouTube HTTP blocking)
106
107 // Track which buffer is being read by main thread
111
112 // State flags
114 bool is_stdin;
116
117 // Stdin I/O context
118 AVIOContext *avio_ctx;
119 unsigned char *avio_buffer;
120
121 // Position tracking
124
125 // Sample-based position tracking
128
129 // Exit signal callback (for graceful shutdown during blocking I/O)
130 bool (*should_exit_callback)(void *user_data);
132};
133
134/* ============================================================================
135 * Stdin I/O Callbacks
136 * ============================================================================ */
137
138/* ============================================================================
139 * Memory-based AVIO for stdin buffering
140 * ============================================================================ */
141
145typedef struct {
147 size_t size;
149
157
161static int memory_read_packet(void *opaque, uint8_t *buf, int buf_size) {
163
164 if (!reader || !reader->buffer || reader->pos >= reader->buffer->size)
165 return AVERROR_EOF;
166
167 int to_read = buf_size;
168 if (reader->pos + to_read > reader->buffer->size)
169 to_read = (int)(reader->buffer->size - reader->pos);
170
171 memcpy(buf, reader->buffer->data + reader->pos, to_read);
172 reader->pos += to_read;
173
174 return to_read;
175}
176
180static int64_t memory_seek_packet(void *opaque, int64_t offset, int whence) {
182
183 if (!reader || !reader->buffer)
184 return -1;
185
186 int64_t new_pos = 0;
187 if (whence == SEEK_SET) {
188 new_pos = offset;
189 } else if (whence == SEEK_CUR) {
190 new_pos = (int64_t)reader->pos + offset;
191 } else if (whence == SEEK_END) {
192 new_pos = (int64_t)reader->buffer->size + offset;
193 } else {
194 return -1;
195 }
196
197 if (new_pos < 0 || new_pos > (int64_t)reader->buffer->size)
198 return -1;
199
200 reader->pos = (size_t)new_pos;
201 return new_pos;
202}
203
208static stdin_buffer_t *stdin_buffer_read_all(void) {
210 if (!sb)
211 return NULL;
212
213 sb->data = NULL;
214 sb->size = 0;
215
216 // Read stdin in 64KB chunks
217 const size_t chunk_size = 65536;
218 uint8_t chunk_buffer[chunk_size];
219 size_t total_capacity = 0;
220 int chunk_count = 0;
221
222 while (1) {
223 size_t bytes_read = fread(chunk_buffer, 1, chunk_size, stdin);
224 if (bytes_read == 0)
225 break;
226 chunk_count++;
227
228 // Grow buffer if needed
229 if (sb->size + bytes_read > total_capacity) {
230 total_capacity = (sb->size + bytes_read) * 2; // Always double capacity
231 uint8_t *new_data = SAFE_MALLOC(total_capacity, uint8_t *);
232 if (!new_data) {
233 SAFE_FREE(sb->data);
234 SAFE_FREE(sb);
235 return NULL;
236 }
237 if (sb->data) {
238 memcpy(new_data, sb->data, sb->size);
239 SAFE_FREE(sb->data);
240 }
241 sb->data = new_data;
242 }
243
244 // Copy chunk to buffer
245 memcpy(sb->data + sb->size, chunk_buffer, bytes_read);
246 sb->size += bytes_read;
247 }
248
249 log_info("Buffered stdin: %zu bytes in %d chunks (capacity: %zu)", sb->size, chunk_count, total_capacity);
250 if (sb->size > 0) {
251 log_info(
252 " First 16 bytes of stdin: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x",
253 sb->data[0], sb->data[1], sb->data[2], sb->data[3], sb->data[4], sb->data[5], sb->data[6], sb->data[7],
254 sb->data[8], sb->data[9], sb->data[10], sb->data[11], sb->data[12], sb->data[13], sb->data[14], sb->data[15]);
255 }
256 return sb;
257}
258
262static stdin_buffer_t *g_stdin_buffer = NULL;
263
264/* ============================================================================
265 * Helper Functions
266 * ============================================================================ */
267
271static inline double av_q2d_safe(AVRational r) {
272 return (r.den != 0) ? ((double)r.num / (double)r.den) : 0.0;
273}
274
278static double get_frame_pts_seconds(AVFrame *frame, AVRational time_base) {
279 if (frame->pts == AV_NOPTS_VALUE) {
280 return -1.0;
281 }
282 return (double)frame->pts * av_q2d_safe(time_base);
283}
284
291static int ffmpeg_interrupt_callback(void *opaque) {
292 ffmpeg_decoder_t *decoder = (ffmpeg_decoder_t *)opaque;
293 if (!decoder) {
294 return 0;
295 }
296
297 // Interrupt av_read_frame() if a seek is in progress
298 if (decoder->seeking_in_progress) {
299 return 1;
300 }
301
302 // Interrupt av_read_frame() if application is shutting down
303 // This allows Ctrl+C to abort blocking I/O operations like YouTube HTTP requests
304 if (decoder->should_exit_callback && decoder->should_exit_callback(decoder->exit_callback_user_data)) {
305 return 1;
306 }
307
308 return 0;
309}
310
318static void *ffmpeg_decoder_prefetch_thread_func(void *arg) {
319 ffmpeg_decoder_t *decoder = (ffmpeg_decoder_t *)arg;
320 if (!decoder || !decoder->prefetch_image_a || !decoder->prefetch_image_b) {
321 return NULL;
322 }
323
324 log_debug("Video prefetch thread started");
325 bool use_image_a = true; // Track which buffer we're using (critical for correct buffer swapping)
326
327 while (true) {
328 mutex_lock(&decoder->prefetch_mutex);
329
330 // Check if thread should stop
331 bool should_stop = decoder->prefetch_should_stop || decoder->eof_reached;
332 if (should_stop) {
333 mutex_unlock(&decoder->prefetch_mutex);
334 break;
335 }
336
337 // Pause if seek is in progress - wait for signal to continue
338 // cond_wait automatically releases and re-acquires mutex
339 while (decoder->seeking_in_progress) {
340 cond_wait(&decoder->prefetch_cond, &decoder->prefetch_mutex);
341 }
342
343 // Mutex is re-acquired after cond_wait - KEEP IT HELD
344
345 // Keep at most one decoded frame pending. Without this check, the producer
346 // can overwrite current_prefetch_image repeatedly and reach EOF before the
347 // consumer has observed the intervening frames.
348 if (decoder->prefetch_frame_ready) {
349 mutex_unlock(&decoder->prefetch_mutex);
351 continue;
352 }
353
354 // Check if the buffer we want to use is still in use by the main thread
355 // If so, skip this iteration and try again later
356 bool buffer_in_use = use_image_a ? decoder->buffer_a_in_use : decoder->buffer_b_in_use;
357 if (buffer_in_use) {
358 // Can't use this buffer yet - main thread is still rendering it
359 mutex_unlock(&decoder->prefetch_mutex);
360 platform_sleep_us(1 * US_PER_MS_INT); // 1ms - brief sleep before retry
361 continue;
362 }
363
364 uint64_t read_start_ns = time_get_ns();
365 bool frame_decoded = false;
366
367 // Release prefetch mutex but hold read_frame_mutex to prevent concurrent av_read_frame() calls from audio thread
368 mutex_unlock(&decoder->prefetch_mutex);
369 mutex_lock(&decoder->read_frame_mutex);
370
371 // Read packets until we get a video frame - read_frame_mutex HELD
372 while (true) {
373 int ret;
374 if (decoder->video_draining) {
375 ret = avcodec_receive_frame(decoder->video_codec_ctx, decoder->frame);
376 if (ret == AVERROR_EOF) {
377 decoder->eof_reached = true;
378 break;
379 }
380 if (ret < 0) {
381 break;
382 }
383 } else {
384 ret = av_read_frame(decoder->format_ctx, decoder->packet);
385 if (ret < 0) {
386 if (ret != AVERROR_EOF) {
387 break;
388 }
389
390 ret = avcodec_send_packet(decoder->video_codec_ctx, NULL);
391 decoder->video_draining = true;
392 if (ret < 0 && ret != AVERROR_EOF) {
393 break;
394 }
395
396 ret = avcodec_receive_frame(decoder->video_codec_ctx, decoder->frame);
397 if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN)) {
398 decoder->eof_reached = true;
399 break;
400 }
401 if (ret < 0) {
402 break;
403 }
404 } else {
405 // Check if this is a video packet
406 if (decoder->packet->stream_index != decoder->video_stream_idx) {
407 av_packet_unref(decoder->packet);
408 continue;
409 }
410
411 // Send packet to decoder
412 ret = avcodec_send_packet(decoder->video_codec_ctx, decoder->packet);
413 av_packet_unref(decoder->packet);
414
415 if (ret < 0) {
416 continue;
417 }
418
419 // Receive decoded frame
420 ret = avcodec_receive_frame(decoder->video_codec_ctx, decoder->frame);
421 if (ret == AVERROR(EAGAIN)) {
422 continue; // Need more packets
423 } else if (ret < 0) {
424 break;
425 }
426 }
427 }
428
429 // Frame decoded - mutex still held
430 // Update position tracking
431 decoder->last_video_pts =
432 get_frame_pts_seconds(decoder->frame, decoder->format_ctx->streams[decoder->video_stream_idx]->time_base);
433
434 // Convert frame to RGB24
435 int width = decoder->video_codec_ctx->width;
436 int height = decoder->video_codec_ctx->height;
437
438 // Get current decode buffer based on which one we're using
439 // (Buffer availability was already checked before entering the decode loop)
440 image_t *decode_buffer = use_image_a ? decoder->prefetch_image_a : decoder->prefetch_image_b;
441
442 // Reallocate if needed (width/height changed)
443 if (decode_buffer->w != width || decode_buffer->h != height) {
444 image_destroy(decode_buffer);
445 decode_buffer = image_new((size_t)width, (size_t)height);
446 if (!decode_buffer) {
447 log_error("Failed to allocate prefetch image buffer");
448 break;
449 }
450 // Update decoder's pointer to the reallocated buffer
451 if (use_image_a) {
452 decoder->prefetch_image_a = decode_buffer;
453 } else {
454 decoder->prefetch_image_b = decode_buffer;
455 }
456 }
457
458 // Convert pixel format (rgb_pixel_t is 3 bytes: r, g, b)
459 uint8_t *dst_data[1] = {(uint8_t *)decode_buffer->pixels};
460 int dst_linesize[1] = {width * 3};
461
462 static int decode_count = 0;
463 if (++decode_count <= 3) {
464 log_info("[FRAME_DECODE] Frame %d: width=%d, height=%d, codec_pix_fmt=%d, frame_pix_fmt=%d (is_stdin=%d)",
465 decode_count, width, height, decoder->video_codec_ctx->pix_fmt, decoder->frame->format,
466 decoder->is_stdin);
467 }
468
469 // For fragmented MP4 from stdin, pixel format might not be set - default to YUV420p
470 // which is standard for H.264/H.265 video codecs
471 int pix_fmt_to_use = decoder->video_codec_ctx->pix_fmt;
472 if (pix_fmt_to_use == AV_PIX_FMT_NONE) {
473 if (decoder->frame->format != AV_PIX_FMT_NONE) {
474 pix_fmt_to_use = decoder->frame->format;
475 log_debug("[SWSCALE_FIX] Using frame->format=%d instead of codec pix_fmt (was NONE)", decoder->frame->format);
476 } else {
477 // Fragmented MP4: Default to YUV420p (standard for H.264/H.265)
478 pix_fmt_to_use = AV_PIX_FMT_YUV420P;
479 log_debug("[SWSCALE_FIX] Fragmented format: Using hardcoded YUV420p (frame->format was also NONE)");
480 }
481 }
482
483 // Lazy initialize swscale context if not done at startup (happens with HTTP/stdin streams)
484 if (!decoder->sws_ctx) {
485 log_debug("Lazy init swscale: width=%d, height=%d, pix_fmt=%d", width, height, pix_fmt_to_use);
486 if (width > 0 && height > 0 && pix_fmt_to_use != AV_PIX_FMT_NONE) {
487 LOG_IO("swscaler", {
488 decoder->sws_ctx = sws_getContext(width, height, pix_fmt_to_use, width, height, AV_PIX_FMT_RGB24,
489 SWS_BILINEAR, NULL, NULL, NULL);
490 });
491 if (!decoder->sws_ctx) {
492 log_error("Failed to create swscale context: pix_fmt=%d (might be unsupported for conversion)",
493 pix_fmt_to_use);
494 break;
495 }
496
497 // Set proper colorspace for H.264 video (BT.709)
498 // sws_setColorspaceDetails(ctx, inv_table, srcRange, table, dstRange, brightness, contrast, saturation)
499 const int *inv_table = sws_getCoefficients(SWS_CS_ITU709);
500 const int *table = sws_getCoefficients(SWS_CS_ITU709);
501 int ret_cs = sws_setColorspaceDetails(decoder->sws_ctx, inv_table, 0, table, 0, 0, 1 << 16, 1 << 16);
502 if (ret_cs < 0) {
503 log_warn("Failed to set BT.709 colorspace details (code=%d), continuing with defaults", ret_cs);
504 } else {
505 log_debug("Set BT.709 colorspace for H.264 YUV->RGB conversion");
506 }
507
508 log_debug("Lazy initialized swscale context with %dx%d from pix_fmt=%d", width, height, pix_fmt_to_use);
509 } else {
510 log_error("Cannot initialize swscale: width=%d, height=%d, pix_fmt=%d (AV_PIX_FMT_NONE=%d)", width, height,
511 pix_fmt_to_use, AV_PIX_FMT_NONE);
512 break;
513 }
514 }
515
516 if (decode_count <= 3) {
517 // Log input YUV data
518 uint8_t *y_data = decoder->frame->data[0];
519 uint8_t *u_data = decoder->frame->data[1];
520 uint8_t *v_data = decoder->frame->data[2];
521 log_info("[YUV_INPUT] Frame %d: Y[0]=%u, U[0]=%u, V[0]=%u (linesize=%d,%d,%d)", decode_count,
522 y_data ? y_data[0] : 255, u_data ? u_data[0] : 255, v_data ? v_data[0] : 255,
523 decoder->frame->linesize[0], decoder->frame->linesize[1], decoder->frame->linesize[2]);
524 }
525
526 int ret_scale = sws_scale(decoder->sws_ctx, (const uint8_t *const *)decoder->frame->data,
527 decoder->frame->linesize, 0, height, dst_data, dst_linesize);
528 if (decode_count <= 3) {
529 log_info("[SWS_SCALE] Frame %d: returned %d", decode_count, ret_scale);
530 rgb_pixel_t *test_px = (rgb_pixel_t *)decode_buffer->pixels;
531 log_info("[SWS_RESULT] Frame %d: RGB[0]=(%u,%u,%u), RGB[100]=(%u,%u,%u)", decode_count, test_px[0].r,
532 test_px[0].g, test_px[0].b, test_px[100].r, test_px[100].g, test_px[100].b);
533 }
534
535 frame_decoded = true;
536 break; // Exit while loop
537 }
538
539 // Release read_frame_mutex now that av_read_frame() is done
541
542 // Re-acquire mutex to update prefetch state
543 mutex_lock(&decoder->prefetch_mutex);
544
545 // Mutex now held - update prefetch state
546 if (frame_decoded) {
547 // Update shared prefetch state while mutex is held
548 }
549
550 mutex_unlock(&decoder->prefetch_mutex); // Release at end of main loop iteration
551
552 if (frame_decoded) {
553 uint64_t read_time_ns = time_elapsed_ns(read_start_ns, time_get_ns());
554
555 // Get current decode buffer
556 image_t *decode_buffer = use_image_a ? decoder->prefetch_image_a : decoder->prefetch_image_b;
557
558 // Update the current prefetch image (main thread will pull from this)
559 mutex_lock(&decoder->prefetch_mutex);
560 decoder->current_prefetch_image = decode_buffer;
561 decoder->prefetch_frame_ready = true;
562 mutex_unlock(&decoder->prefetch_mutex);
563
564 char read_time_str[32];
565 time_pretty(read_time_ns, -1, read_time_str, sizeof(read_time_str));
566 log_dev_every(5 * US_PER_SEC_INT, "PREFETCH: decoded frame in %s", read_time_str);
567
568 // Switch to the other buffer for next iteration (MUST use boolean flag, not pointer comparison)
569 use_image_a = !use_image_a;
570 } else {
571 // EOF or error - exit thread
572 break;
573 }
574 }
575
576 log_debug("Video prefetch thread stopped");
577 return NULL;
578}
579
583static asciichat_error_t open_codec_context(AVFormatContext *fmt_ctx, enum AVMediaType type, int *stream_idx,
584 AVCodecContext **codec_ctx) {
585 int ret = av_find_best_stream(fmt_ctx, type, -1, -1, NULL, 0);
586 if (ret < 0) {
587 // Stream not found - not an error, just means no stream of this type
588 *stream_idx = -1;
589 *codec_ctx = NULL;
590 return ASCIICHAT_OK;
591 }
592
593 *stream_idx = ret;
594 AVStream *stream = fmt_ctx->streams[ret];
595
596 // Find decoder
597 const AVCodec *codec = avcodec_find_decoder(stream->codecpar->codec_id);
598 if (!codec) {
599 return SET_ERRNO(ERROR_MEDIA_DECODE, "Codec not found for stream %d", ret);
600 }
601
602 // Allocate codec context
603 *codec_ctx = avcodec_alloc_context3(codec);
604 if (!*codec_ctx) {
605 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate codec context");
606 }
607
608 // Copy codec parameters
609 if (avcodec_parameters_to_context(*codec_ctx, stream->codecpar) < 0) {
610 avcodec_free_context(codec_ctx);
611 return SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to copy codec parameters");
612 }
613
614 // Open codec
615 if (avcodec_open2(*codec_ctx, codec, NULL) < 0) {
616 avcodec_free_context(codec_ctx);
617 return SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to open codec");
618 }
619
620 return ASCIICHAT_OK;
621}
622
623/* ============================================================================
624 * Decoder Lifecycle
625 * ============================================================================ */
626
628 if (!path) {
629 SET_ERRNO(ERROR_INVALID_PARAM, "Path is NULL");
630 return NULL;
631 }
632
633 // Suppress FFmpeg's verbose debug logging (H.264 codec warnings, etc.)
634 // Only set this once, it's a global setting
635 static bool ffmpeg_log_level_set = false;
636 if (!ffmpeg_log_level_set) {
637 av_log_set_level(AV_LOG_QUIET); // Suppress all FFmpeg logging
638 av_log_set_callback(ffmpeg_silent_log_callback); // Install silent callback to discard all output
639 ffmpeg_log_level_set = true;
640 }
641
643 if (!decoder) {
644 SET_ERRNO(ERROR_MEMORY, "Failed to allocate decoder");
645 return NULL;
646 }
647
648 memset(decoder, 0, sizeof(*decoder));
649 decoder->video_stream_idx = -1;
650 decoder->audio_stream_idx = -1;
651 decoder->last_video_pts = -1.0;
652 decoder->last_audio_pts = -1.0;
653
654 // Capture FFmpeg's probing output (FFmpeg may write directly to either stream)
655 int ret = 0;
656 LOG_IO("ffmpeg", {
657 // Configure FFmpeg options for HTTP streaming performance
658 AVDictionary *options = NULL;
659
660 // For HTTP/HTTPS streams: enable fast probing and reconnection (validated via production-grade URL regex)
661 if (path && url_is_valid(path)) {
662 // Limit probing to 32KB for faster format detection
663 av_dict_set(&options, "probesize", "32768", 0);
664 // Analyze for 100ms max to determine streams quickly
665 av_dict_set(&options, "analyzeduration", "100000", 0);
666 // Enable auto-reconnection for interrupted connections
667 av_dict_set(&options, "reconnect", "1", 0);
668 // Allow reconnection for streamed protocols
669 av_dict_set(&options, "reconnect_streamed", "1", 0);
670 // Set reasonable I/O timeout (10 seconds)
671 av_dict_set(&options, "rw_timeout", "10000000", 0);
672 // Enable HTTP persistent connection (keep-alive) for better performance
673 av_dict_set(&options, "http_persistent", "1", 0);
674 // Reduce connect timeout to fail faster if server is unreachable
675 av_dict_set(&options, "connect_timeout", "5000000", 0);
676 }
677
678 // Open input file
679 ret = avformat_open_input(&decoder->format_ctx, path, NULL, &options);
680 av_dict_free(&options); // Free options dictionary
681
682 if (ret >= 0) {
683 // Find stream info
684 if (avformat_find_stream_info(decoder->format_ctx, NULL) < 0) {
685 ret = -1;
686 }
687 }
688 });
689
690 if (ret < 0) {
691 if (ret == -1) {
692 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to find stream info");
693 } else {
694 SET_ERRNO(ERROR_MEDIA_OPEN, "Failed to open media file: %s", path);
695 }
696 if (decoder->format_ctx) {
697 avformat_close_input(&decoder->format_ctx);
698 }
699 SAFE_FREE(decoder);
700 return NULL;
701 }
702
703 // Install interrupt callback to allow seeking to interrupt long av_read_frame() calls
704 // Do this AFTER finding stream info to ensure format context is fully initialized
705 if (decoder->format_ctx) {
706 decoder->format_ctx->interrupt_callback.callback = ffmpeg_interrupt_callback;
707 decoder->format_ctx->interrupt_callback.opaque = decoder;
708 }
709
710 // Open video codec
711 asciichat_error_t err = open_codec_context(decoder->format_ctx, AVMEDIA_TYPE_VIDEO, &decoder->video_stream_idx,
712 &decoder->video_codec_ctx);
713 if (err != ASCIICHAT_OK) {
714 log_warn("Failed to open video codec (file may be audio-only)");
715 }
716
717 // Open audio codec - audio is enabled by default (no option needed)
718 // Always try to open audio codec, don't rely on GET_OPTION(audio_enabled) which has a default issue
719 err = open_codec_context(decoder->format_ctx, AVMEDIA_TYPE_AUDIO, &decoder->audio_stream_idx,
720 &decoder->audio_codec_ctx);
721 if (err != ASCIICHAT_OK) {
722 log_debug("No audio codec found (file may be video-only or audio codec not available)");
723 decoder->audio_stream_idx = -1;
724 decoder->audio_codec_ctx = NULL;
725 }
726
727 // Require at least one stream
728 if (decoder->video_stream_idx < 0 && decoder->audio_stream_idx < 0) {
729 SET_ERRNO(ERROR_MEDIA_DECODE, "No video or audio streams found");
730 ffmpeg_decoder_destroy(decoder);
731 return NULL;
732 }
733
734 // Allocate frame and packet
735 decoder->frame = av_frame_alloc();
736 decoder->packet = av_packet_alloc();
737 if (!decoder->frame || !decoder->packet) {
738 SET_ERRNO(ERROR_MEMORY, "Failed to allocate frame/packet");
739 ffmpeg_decoder_destroy(decoder);
740 return NULL;
741 }
742
743 // Initialize swscale context for video if present
744 if (decoder->video_codec_ctx) {
745 // Validate codec context has valid dimensions and pixel format
746 // For HTTP streams, these might not be valid until first frame is read
747 if (decoder->video_codec_ctx->width <= 0 || decoder->video_codec_ctx->height <= 0) {
748 log_warn("Video codec has invalid dimensions (%dx%d), will initialize swscale on first frame",
749 decoder->video_codec_ctx->width, decoder->video_codec_ctx->height);
750 // Don't create swscale context yet - will create it lazily on first frame read
751 } else if (decoder->video_codec_ctx->pix_fmt == AV_PIX_FMT_NONE) {
752 log_warn("Video codec has invalid pixel format, will initialize swscale on first frame");
753 // Don't create swscale context yet - will create it lazily on first frame read
754 } else {
755 // Create swscale context with valid parameters
756 LOG_IO("swscaler", {
757 decoder->sws_ctx =
758 sws_getContext(decoder->video_codec_ctx->width, decoder->video_codec_ctx->height,
759 decoder->video_codec_ctx->pix_fmt, decoder->video_codec_ctx->width,
760 decoder->video_codec_ctx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
761 });
762 if (!decoder->sws_ctx) {
763 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to create swscale context");
764 ffmpeg_decoder_destroy(decoder);
765 return NULL;
766 }
767 }
768 }
769
770 // Initialize swresample context for audio if present
771 if (decoder->audio_codec_ctx) {
772 // Store output sample rate for position tracking
774
775 // Allocate resampler context
776 decoder->swr_ctx = swr_alloc();
777 if (!decoder->swr_ctx) {
778 SET_ERRNO(ERROR_MEMORY, "Failed to allocate swresample context");
779 ffmpeg_decoder_destroy(decoder);
780 return NULL;
781 }
782
783 // Set options
784 av_opt_set_chlayout(decoder->swr_ctx, "in_chlayout", &decoder->audio_codec_ctx->ch_layout, 0);
785 av_opt_set_int(decoder->swr_ctx, "in_sample_rate", decoder->audio_codec_ctx->sample_rate, 0);
786 av_opt_set_sample_fmt(decoder->swr_ctx, "in_sample_fmt", decoder->audio_codec_ctx->sample_fmt, 0);
787
788 AVChannelLayout out_ch_layout = AV_CHANNEL_LAYOUT_MONO;
789 av_opt_set_chlayout(decoder->swr_ctx, "out_chlayout", &out_ch_layout, 0);
790 av_opt_set_int(decoder->swr_ctx, "out_sample_rate", TARGET_SAMPLE_RATE, 0);
791 av_opt_set_sample_fmt(decoder->swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
792
793 // Initialize
794 if (swr_init(decoder->swr_ctx) < 0) {
795 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to initialize swresample context");
796 ffmpeg_decoder_destroy(decoder);
797 return NULL;
798 }
799
800 // Allocate audio buffer (10 seconds worth)
802 decoder->audio_buffer = SAFE_MALLOC(decoder->audio_buffer_size * sizeof(float), float *);
803 if (!decoder->audio_buffer) {
804 SET_ERRNO(ERROR_MEMORY, "Failed to allocate audio buffer");
805 ffmpeg_decoder_destroy(decoder);
806 return NULL;
807 }
808 }
809
810 // Initialize video frame prefetching system (for YouTube streaming)
811 if (decoder->video_stream_idx >= 0) {
812 int width = decoder->video_codec_ctx->width;
813 int height = decoder->video_codec_ctx->height;
814
815 // Create two prefetch image buffers for double-buffering
816 decoder->prefetch_image_a = image_new((size_t)width, (size_t)height);
817 decoder->prefetch_image_b = image_new((size_t)width, (size_t)height);
818 if (!decoder->prefetch_image_a || !decoder->prefetch_image_b) {
819 SET_ERRNO(ERROR_MEMORY, "Failed to allocate prefetch image buffers");
820 ffmpeg_decoder_destroy(decoder);
821 return NULL;
822 }
823
824 decoder->current_prefetch_image = decoder->prefetch_image_a;
825 decoder->prefetch_frame_ready = false;
826
827 // Initialize prefetch mutex
828 if (mutex_init(&decoder->prefetch_mutex, "ffmpeg_prefetch") != 0) {
829 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch mutex");
830 ffmpeg_decoder_destroy(decoder);
831 return NULL;
832 }
833
834 if (cond_init(&decoder->prefetch_cond, "ffmpeg_prefetch") != 0) {
835 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch condition variable");
836 mutex_destroy(&decoder->prefetch_mutex);
837 ffmpeg_decoder_destroy(decoder);
838 return NULL;
839 }
840
841 if (mutex_init(&decoder->read_frame_mutex, "ffmpeg_read_frame") != 0) {
842 SET_ERRNO(ERROR_MEMORY, "Failed to initialize read_frame mutex");
843 mutex_destroy(&decoder->prefetch_mutex);
844 cond_destroy(&decoder->prefetch_cond);
845 ffmpeg_decoder_destroy(decoder);
846 return NULL;
847 }
848
849 decoder->prefetch_thread_running = false;
850 decoder->prefetch_should_stop = false;
851 }
852
853 log_debug("FFmpeg decoder opened: %s (video=%s, audio=%s)", path, decoder->video_stream_idx >= 0 ? "yes" : "no",
854 decoder->audio_stream_idx >= 0 ? "yes" : "no");
855
856 char decoder_name[256];
857 snprintf(decoder_name, sizeof(decoder_name), "%s", path);
858 NAMED_REGISTER_FFMPEG_DECODER(decoder, decoder_name, NULL);
859
860 return decoder;
861}
862
864 // Buffer entire stdin on first call (shared between video and audio decoders)
865 if (!g_stdin_buffer) {
866 log_debug("Reading stdin into buffer...");
867 g_stdin_buffer = stdin_buffer_read_all();
868 if (!g_stdin_buffer) {
869 SET_ERRNO(ERROR_MEDIA_OPEN, "Failed to read stdin into buffer");
870 return NULL;
871 }
872 }
873
875 if (!decoder) {
876 SET_ERRNO(ERROR_MEMORY, "Failed to allocate decoder");
877 return NULL;
878 }
879
880 memset(decoder, 0, sizeof(*decoder));
881 decoder->video_stream_idx = -1;
882 decoder->audio_stream_idx = -1;
883 decoder->is_stdin = true;
884 decoder->last_video_pts = -1.0;
885 decoder->last_audio_pts = -1.0;
886
887 // Create per-decoder reader (each decoder gets independent position tracking)
889 if (!reader) {
890 SET_ERRNO(ERROR_MEMORY, "Failed to allocate buffer reader");
891 SAFE_FREE(decoder);
892 return NULL;
893 }
894 reader->buffer = g_stdin_buffer;
895 reader->pos = 0; // Each decoder starts from position 0
896
897 // AVIO takes ownership of this buffer and may resize or free it with av_free().
898 decoder->avio_buffer = av_malloc(AVIO_BUFFER_SIZE);
899 if (!decoder->avio_buffer) {
900 SET_ERRNO(ERROR_MEMORY, "Failed to allocate AVIO buffer");
901 SAFE_FREE(reader);
902 SAFE_FREE(decoder);
903 return NULL;
904 }
905
906 decoder->avio_ctx = avio_alloc_context(decoder->avio_buffer, // internal buffer for AVIO
907 AVIO_BUFFER_SIZE, // buffer size
908 0, // write_flag
909 reader, // opaque (per-decoder reader)
910 memory_read_packet,
911 NULL, // write_packet
912 memory_seek_packet // seek (memory is seekable)
913 );
914
915 if (!decoder->avio_ctx) {
916 SET_ERRNO(ERROR_MEMORY, "Failed to create AVIO context");
917 av_free(decoder->avio_buffer);
918 decoder->avio_buffer = NULL;
919 SAFE_FREE(decoder);
920 return NULL;
921 }
922
923 // Mark AVIO context as seekable (memory buffer supports seeking)
924 decoder->avio_ctx->seekable = AVIO_SEEKABLE_NORMAL;
925
926 // Allocate format context
927 decoder->format_ctx = avformat_alloc_context();
928 if (!decoder->format_ctx) {
929 SET_ERRNO(ERROR_MEMORY, "Failed to allocate format context");
930 av_freep(&decoder->avio_ctx->buffer);
931 avio_context_free(&decoder->avio_ctx);
932 SAFE_FREE(decoder);
933 return NULL;
934 }
935
936 decoder->format_ctx->pb = decoder->avio_ctx;
937 decoder->format_ctx->flags |= AVFMT_FLAG_CUSTOM_IO; // Tell FFmpeg to use our custom AVIO context
938
939 // Capture FFmpeg's probing output
940 int ret = 0;
941 LOG_IO("ffmpeg", {
942 // Open input from buffered stdin
943 ret = avformat_open_input(&decoder->format_ctx, NULL, NULL, NULL);
944 if (ret >= 0) {
945 // Find stream info
946 if (avformat_find_stream_info(decoder->format_ctx, NULL) < 0) {
947 ret = -1;
948 }
949 }
950 });
951
952 if (ret < 0) {
953 if (ret == -1) {
954 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to find stream info from stdin buffer");
955 } else {
956 SET_ERRNO(ERROR_MEDIA_OPEN, "Failed to open stdin buffer");
957 }
958 av_freep(&decoder->avio_ctx->buffer);
959 avio_context_free(&decoder->avio_ctx);
960 if (decoder->format_ctx) {
961 avformat_free_context(decoder->format_ctx);
962 }
963 SAFE_FREE(decoder);
964 return NULL;
965 }
966
967 // Open codecs (same as file-based decoder)
968 asciichat_error_t err = open_codec_context(decoder->format_ctx, AVMEDIA_TYPE_VIDEO, &decoder->video_stream_idx,
969 &decoder->video_codec_ctx);
970 if (err != ASCIICHAT_OK) {
971 log_warn("Failed to open video codec from stdin");
972 }
973
974 if (GET_OPTION(audio_enabled)) {
975 err = open_codec_context(decoder->format_ctx, AVMEDIA_TYPE_AUDIO, &decoder->audio_stream_idx,
976 &decoder->audio_codec_ctx);
977 if (err != ASCIICHAT_OK) {
978 log_warn("Failed to open audio codec from stdin");
979 }
980 } else {
981 decoder->audio_stream_idx = -1;
982 decoder->audio_codec_ctx = NULL;
983 log_debug("Audio decoding disabled by user option");
984 }
985
986 if (decoder->video_stream_idx < 0 && decoder->audio_stream_idx < 0) {
987 SET_ERRNO(ERROR_MEDIA_DECODE, "No video or audio streams found in stdin");
988 ffmpeg_decoder_destroy(decoder);
989 return NULL;
990 }
991
992 // Allocate frame and packet
993 decoder->frame = av_frame_alloc();
994 decoder->packet = av_packet_alloc();
995 if (!decoder->frame || !decoder->packet) {
996 SET_ERRNO(ERROR_MEMORY, "Failed to allocate frame/packet");
997 ffmpeg_decoder_destroy(decoder);
998 return NULL;
999 }
1000
1001 // Initialize swscale/swresample (same as file-based)
1002 if (decoder->video_codec_ctx) {
1003 // Validate codec context has valid dimensions and pixel format
1004 // For stdin/HTTP streams, these might not be valid until first frame is read
1005 if (decoder->video_codec_ctx->width <= 0 || decoder->video_codec_ctx->height <= 0) {
1006 log_warn("Video codec has invalid dimensions (%dx%d), will initialize swscale on first frame",
1007 decoder->video_codec_ctx->width, decoder->video_codec_ctx->height);
1008 // Don't create swscale context yet - will create it lazily on first frame read
1009 } else if (decoder->video_codec_ctx->pix_fmt == AV_PIX_FMT_NONE) {
1010 log_warn("Video codec has invalid pixel format, will initialize swscale on first frame");
1011 // Don't create swscale context yet - will create it lazily on first frame read
1012 } else {
1013 // Create swscale context with valid parameters
1014 LOG_IO("swscaler", {
1015 decoder->sws_ctx =
1016 sws_getContext(decoder->video_codec_ctx->width, decoder->video_codec_ctx->height,
1017 decoder->video_codec_ctx->pix_fmt, decoder->video_codec_ctx->width,
1018 decoder->video_codec_ctx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
1019 });
1020 if (!decoder->sws_ctx) {
1021 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to create swscale context");
1022 ffmpeg_decoder_destroy(decoder);
1023 return NULL;
1024 }
1025 }
1026 }
1027
1028 if (decoder->audio_codec_ctx) {
1029 decoder->swr_ctx = swr_alloc();
1030 if (!decoder->swr_ctx) {
1031 SET_ERRNO(ERROR_MEMORY, "Failed to allocate swresample context");
1032 ffmpeg_decoder_destroy(decoder);
1033 return NULL;
1034 }
1035
1036 av_opt_set_chlayout(decoder->swr_ctx, "in_chlayout", &decoder->audio_codec_ctx->ch_layout, 0);
1037 av_opt_set_int(decoder->swr_ctx, "in_sample_rate", decoder->audio_codec_ctx->sample_rate, 0);
1038 av_opt_set_sample_fmt(decoder->swr_ctx, "in_sample_fmt", decoder->audio_codec_ctx->sample_fmt, 0);
1039
1040 AVChannelLayout out_ch_layout = AV_CHANNEL_LAYOUT_MONO;
1041 av_opt_set_chlayout(decoder->swr_ctx, "out_chlayout", &out_ch_layout, 0);
1042 av_opt_set_int(decoder->swr_ctx, "out_sample_rate", TARGET_SAMPLE_RATE, 0);
1043 av_opt_set_sample_fmt(decoder->swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
1044
1045 if (swr_init(decoder->swr_ctx) < 0) {
1046 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to initialize swresample context");
1047 ffmpeg_decoder_destroy(decoder);
1048 return NULL;
1049 }
1050
1051 decoder->audio_buffer_size = TARGET_SAMPLE_RATE * 10;
1052 decoder->audio_buffer = SAFE_MALLOC(decoder->audio_buffer_size * sizeof(float), float *);
1053 if (!decoder->audio_buffer) {
1054 SET_ERRNO(ERROR_MEMORY, "Failed to allocate audio buffer");
1055 ffmpeg_decoder_destroy(decoder);
1056 return NULL;
1057 }
1058 }
1059
1060 // Initialize video frame prefetching system for stdin (same as file-based)
1061 if (decoder->video_stream_idx >= 0) {
1062 int width = decoder->video_codec_ctx->width;
1063 int height = decoder->video_codec_ctx->height;
1064
1065 // For stdin sources, dimensions might be invalid initially (0x0)
1066 // Use default dimensions for prefetch buffers; they'll be resized on first frame
1067 if (width <= 0 || height <= 0) {
1068 width = 1920; // Default width for stdin
1069 height = 1080; // Default height for stdin
1070 log_debug("stdin: Using default prefetch dimensions %dx%d (will be resized on first frame)", width, height);
1071 }
1072
1073 // Create two prefetch image buffers for double-buffering
1074 decoder->prefetch_image_a = image_new((size_t)width, (size_t)height);
1075 decoder->prefetch_image_b = image_new((size_t)width, (size_t)height);
1076 if (!decoder->prefetch_image_a || !decoder->prefetch_image_b) {
1077 SET_ERRNO(ERROR_MEMORY, "Failed to allocate prefetch image buffers");
1078 ffmpeg_decoder_destroy(decoder);
1079 return NULL;
1080 }
1081
1082 decoder->current_prefetch_image = decoder->prefetch_image_a;
1083 decoder->prefetch_frame_ready = false;
1084
1085 // Initialize prefetch mutex
1086 if (mutex_init(&decoder->prefetch_mutex, "ffmpeg_prefetch") != 0) {
1087 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch mutex");
1088 ffmpeg_decoder_destroy(decoder);
1089 return NULL;
1090 }
1091
1092 if (cond_init(&decoder->prefetch_cond, "ffmpeg_prefetch") != 0) {
1093 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch condition variable");
1094 mutex_destroy(&decoder->prefetch_mutex);
1095 ffmpeg_decoder_destroy(decoder);
1096 return NULL;
1097 }
1098
1099 if (mutex_init(&decoder->read_frame_mutex, "ffmpeg_read_frame") != 0) {
1100 SET_ERRNO(ERROR_MEMORY, "Failed to initialize read_frame mutex");
1101 mutex_destroy(&decoder->prefetch_mutex);
1102 cond_destroy(&decoder->prefetch_cond);
1103 ffmpeg_decoder_destroy(decoder);
1104 return NULL;
1105 }
1106 }
1107
1108 log_debug("FFmpeg decoder opened from stdin (video=%s, audio=%s)", decoder->video_stream_idx >= 0 ? "yes" : "no",
1109 decoder->audio_stream_idx >= 0 ? "yes" : "no");
1110
1111 NAMED_REGISTER_FFMPEG_DECODER(decoder, "stdin", NULL);
1112
1113 return decoder;
1114}
1115
1117 if (!decoder) {
1118 return;
1119 }
1120
1121 NAMED_UNREGISTER(decoder);
1122
1123 // Stop prefetch thread (signal it to stop and wait for it to finish)
1124 if (decoder->prefetch_thread_running) {
1125 mutex_lock(&decoder->prefetch_mutex);
1126 decoder->prefetch_should_stop = true;
1127 mutex_unlock(&decoder->prefetch_mutex);
1128
1129 // Wait for thread to finish
1130 asciichat_thread_join(&decoder->prefetch_thread, NULL);
1131 decoder->prefetch_thread_running = false;
1132 }
1133
1134 // Clean up prefetch state
1135 cond_destroy(&decoder->prefetch_cond);
1136 mutex_destroy(&decoder->prefetch_mutex);
1138
1139 // Free prefetch image buffers
1140 if (decoder->prefetch_image_a) {
1142 decoder->prefetch_image_a = NULL;
1143 }
1144 if (decoder->prefetch_image_b) {
1146 decoder->prefetch_image_b = NULL;
1147 }
1148
1149 // Don't destroy current_image - it points to one of the prefetch buffers
1150 // which have already been destroyed above
1151 decoder->current_image = NULL;
1152
1153 // Free audio buffer
1154 SAFE_FREE(decoder->audio_buffer);
1155
1156 // Free swscale context
1157 if (decoder->sws_ctx) {
1158 sws_freeContext(decoder->sws_ctx);
1159 decoder->sws_ctx = NULL;
1160 }
1161
1162 // Free swresample context
1163 if (decoder->swr_ctx) {
1164 swr_free(&decoder->swr_ctx);
1165 }
1166
1167 // Free frame and packet
1168 if (decoder->frame) {
1169 av_frame_free(&decoder->frame);
1170 }
1171 if (decoder->packet) {
1172 av_packet_free(&decoder->packet);
1173 }
1174
1175 // Free codec contexts
1176 if (decoder->video_codec_ctx) {
1177 avcodec_free_context(&decoder->video_codec_ctx);
1178 }
1179 if (decoder->audio_codec_ctx) {
1180 avcodec_free_context(&decoder->audio_codec_ctx);
1181 }
1182
1183 // Free format context
1184 if (decoder->format_ctx) {
1185 avformat_close_input(&decoder->format_ctx);
1186 }
1187
1188 // Free AVIO context (stdin only)
1189 if (decoder->avio_ctx) {
1190 // Free the opaque reader for stdin
1191 if (decoder->is_stdin) {
1192 SAFE_FREE(decoder->avio_ctx->opaque);
1193 }
1194 av_freep(&decoder->avio_ctx->buffer);
1195 avio_context_free(&decoder->avio_ctx);
1196 }
1197
1198 SAFE_FREE(decoder);
1199}
1200
1201/* ============================================================================
1202 * Video Operations
1203 * ============================================================================ */
1204
1206 if (!decoder || decoder->video_stream_idx < 0) {
1207 return NULL;
1208 }
1209
1210 // Try to get a prefetched frame from the background thread (preferred path)
1211 mutex_lock(&decoder->prefetch_mutex);
1212 if (decoder->prefetch_frame_ready && decoder->current_prefetch_image) {
1213 // Release the previous buffer (rendering is now complete)
1214 if (decoder->current_read_buffer == decoder->prefetch_image_a) {
1215 decoder->buffer_a_in_use = false;
1216 } else if (decoder->current_read_buffer == decoder->prefetch_image_b) {
1217 decoder->buffer_b_in_use = false;
1218 }
1219
1221 decoder->prefetch_frame_ready = false;
1222
1223 // Mark the new buffer as in use (prevent prefetch thread from overwriting it during rendering)
1224 if (frame == decoder->prefetch_image_a) {
1225 decoder->buffer_a_in_use = true;
1226 } else if (frame == decoder->prefetch_image_b) {
1227 decoder->buffer_b_in_use = true;
1228 }
1229
1230 decoder->current_read_buffer = frame;
1231 mutex_unlock(&decoder->prefetch_mutex);
1232
1233 // Use the prefetched frame
1234 decoder->current_image = frame;
1235 log_dev_every(5 * US_PER_SEC_INT, "Using prefetched frame");
1236 return frame;
1237 }
1238 mutex_unlock(&decoder->prefetch_mutex);
1239
1240 // No fallback synchronous decode - rely on background prefetch thread
1241 // Skipping frames when prefetch not ready allows audio timing to advance
1242 // This is critical for proper audio-video sync when prefetch is active
1244 "Prefetch frame not ready, skipping to next iteration (allow prefetch to catch up)");
1245 return NULL;
1246}
1247
1256 if (!decoder || decoder->video_stream_idx < 0) {
1257 return ERROR_INVALID_PARAM;
1258 }
1259
1260 if (!decoder->prefetch_image_a || !decoder->prefetch_image_b) {
1261 return ERROR_INVALID_PARAM;
1262 }
1263
1264 // Already running
1265 if (decoder->prefetch_thread_running) {
1266 return ASCIICHAT_OK;
1267 }
1268
1269 // Reset stop flag and create thread
1270 decoder->prefetch_should_stop = false;
1271
1272 int thread_err = asciichat_thread_create(&decoder->prefetch_thread, "ffmpeg_prefetch",
1273 ffmpeg_decoder_prefetch_thread_func, decoder);
1274 if (thread_err != 0) {
1275 return SET_ERRNO(ERROR_THREAD, "Failed to create video prefetch thread");
1276 }
1277
1278 decoder->prefetch_thread_running = true;
1279 return ASCIICHAT_OK;
1280}
1281
1286 if (!decoder || !decoder->prefetch_thread_running) {
1287 return;
1288 }
1289
1290 decoder->prefetch_should_stop = true;
1291 // Wait up to 2 seconds for thread to stop
1292 // The interrupt callback should cause av_read_frame to abort quickly
1294
1295 // Mark thread as stopped regardless of join result
1296 // (if join succeeded: cleanup was done; if timeout: will be cleaned up on restart)
1297 decoder->prefetch_thread_running = false;
1298}
1299
1300void ffmpeg_decoder_set_exit_callback(ffmpeg_decoder_t *decoder, bool (*should_exit_callback)(void *),
1301 void *user_data) {
1302 if (!decoder) {
1303 return;
1304 }
1305 decoder->should_exit_callback = should_exit_callback;
1306 decoder->exit_callback_user_data = user_data;
1307}
1308
1310 if (!decoder) {
1311 return false;
1312 }
1313 return decoder->prefetch_thread_running;
1314}
1315
1317 return decoder && decoder->video_stream_idx >= 0;
1318}
1319
1321 if (!decoder || decoder->video_stream_idx < 0) {
1322 return ERROR_INVALID_PARAM;
1323 }
1324
1325 if (width) {
1326 *width = decoder->video_codec_ctx->width;
1327 }
1328 if (height) {
1329 *height = decoder->video_codec_ctx->height;
1330 }
1331
1332 return ASCIICHAT_OK;
1333}
1334
1336 if (!decoder || decoder->video_stream_idx < 0) {
1337 return -1.0;
1338 }
1339
1340 AVStream *stream = decoder->format_ctx->streams[decoder->video_stream_idx];
1341
1342 // Try avg_frame_rate first (average frame rate from entire stream)
1343 double fps = av_q2d_safe(stream->avg_frame_rate);
1344
1345 // Fallback to r_frame_rate if avg_frame_rate is invalid or zero
1346 // r_frame_rate is the "real" frame rate based on codec parameters
1347 // This is more reliable for YouTube videos and some video codecs
1348 if (fps <= 0.0) {
1349 fps = av_q2d_safe(stream->r_frame_rate);
1350 }
1351
1352 return fps;
1353}
1354
1355/* ============================================================================
1356 * Audio Operations
1357 * ============================================================================ */
1358
1359size_t ffmpeg_decoder_read_audio_samples(ffmpeg_decoder_t *decoder, float *buffer, size_t num_samples) {
1360 if (!decoder || decoder->audio_stream_idx < 0 || !buffer || num_samples == 0) {
1361 return 0;
1362 }
1363
1364 size_t samples_written = 0;
1365
1366 // First, drain any buffered samples
1367 if (decoder->audio_buffer_offset > 0) {
1368 size_t available = decoder->audio_buffer_offset;
1369 size_t to_copy = (available < num_samples) ? available : num_samples;
1370
1371 memcpy(buffer, decoder->audio_buffer, to_copy * sizeof(float));
1372 samples_written += to_copy;
1373
1374 // Shift buffer
1375 if (to_copy < available) {
1376 memmove(decoder->audio_buffer, decoder->audio_buffer + to_copy, (available - to_copy) * sizeof(float));
1377 }
1378 decoder->audio_buffer_offset -= to_copy;
1379
1380 if (samples_written >= num_samples) {
1381 decoder->audio_samples_read += samples_written;
1382 return samples_written;
1383 }
1384 }
1385
1386 // Read more packets to fill the request
1387 static uint64_t packet_count = 0;
1388
1389 // Lock read_frame_mutex to prevent concurrent av_read_frame() calls from video prefetch thread
1390 mutex_lock(&decoder->read_frame_mutex);
1391
1392 while (samples_written < num_samples) {
1393 int ret = av_read_frame(decoder->format_ctx, decoder->packet);
1394 if (ret < 0) {
1395 if (ret == AVERROR_EOF) {
1396 decoder->eof_reached = true;
1397 }
1398 break;
1399 }
1400
1401 // Check if this is an audio packet
1402 if (decoder->packet->stream_index != decoder->audio_stream_idx) {
1403 av_packet_unref(decoder->packet);
1404 continue;
1405 }
1406
1407 log_info_every(50 * US_PER_MS_INT, "Audio packet #%lu: pts=%ld dts=%ld duration=%d size=%d", packet_count++,
1408 decoder->packet->pts, decoder->packet->dts, decoder->packet->duration, decoder->packet->size);
1409
1410 // Send packet to decoder
1411 ret = avcodec_send_packet(decoder->audio_codec_ctx, decoder->packet);
1412 av_packet_unref(decoder->packet);
1413
1414 if (ret < 0) {
1415 char error_message[AV_ERROR_MAX_STRING_SIZE];
1416 av_strerror(ret, error_message, sizeof(error_message));
1417 log_warn("Error sending audio packet to decoder: %s", error_message);
1418 continue;
1419 }
1420
1421 // Receive all decoded frames from this packet
1422 // Important: a single packet can produce multiple frames. Must drain all before next packet.
1423 while (1) {
1424 ret = avcodec_receive_frame(decoder->audio_codec_ctx, decoder->frame);
1425 if (ret == AVERROR(EAGAIN)) {
1426 break; // No more frames from this packet, get next packet
1427 } else if (ret < 0) {
1428 log_warn("Error receiving audio frame from decoder");
1429 goto audio_read_done;
1430 }
1431
1432 // Update position tracking
1433 decoder->last_audio_pts =
1434 get_frame_pts_seconds(decoder->frame, decoder->format_ctx->streams[decoder->audio_stream_idx]->time_base);
1435
1436 // Resample the complete frame and retain output beyond this callback's
1437 // requested block. Flushing the resampler between packets would insert
1438 // padding into inputs whose sample rate differs from the output rate.
1439 uint8_t *out_ptr = (uint8_t *)decoder->audio_buffer;
1440 int converted = swr_convert(decoder->swr_ctx, &out_ptr, (int)decoder->audio_buffer_size,
1441 (const uint8_t **)decoder->frame->data, decoder->frame->nb_samples);
1442
1443 if (converted > 0) {
1444 size_t available = (size_t)converted;
1445 size_t remaining = num_samples - samples_written;
1446 size_t to_copy = available < remaining ? available : remaining;
1447 memcpy(buffer + samples_written, decoder->audio_buffer, to_copy * sizeof(float));
1448 samples_written += to_copy;
1449 decoder->audio_buffer_offset = available - to_copy;
1450 if (decoder->audio_buffer_offset > 0)
1451 memmove(decoder->audio_buffer, decoder->audio_buffer + to_copy, decoder->audio_buffer_offset * sizeof(float));
1452 }
1453
1454 if (samples_written >= num_samples) {
1455 goto audio_read_done;
1456 }
1457 }
1458 }
1459
1460audio_read_done:
1461 // Release read_frame_mutex now that av_read_frame() is done
1462 mutex_unlock(&decoder->read_frame_mutex);
1463
1464 // Flush resampler buffer if we haven't filled the full request
1465 // The resampler may have buffered samples that need to be output
1466 if (samples_written < num_samples && decoder->eof_reached) {
1467 int remaining_space = (int)(num_samples - samples_written);
1468 uint8_t *out_ptr = (uint8_t *)(buffer + samples_written);
1469 int flushed = swr_convert(decoder->swr_ctx, &out_ptr, remaining_space, NULL, 0);
1470 if (flushed > 0) {
1471 samples_written += (size_t)flushed;
1472 }
1473 }
1474
1475 // Update sample-based position tracking
1476 decoder->audio_samples_read += samples_written;
1477
1478 return samples_written;
1479}
1480
1482 return decoder && decoder->audio_stream_idx >= 0;
1483}
1484
1485/* ============================================================================
1486 * Playback Control
1487 * ============================================================================ */
1488
1490 if (!decoder) {
1491 return ERROR_INVALID_PARAM;
1492 }
1493
1494 if (decoder->is_stdin) {
1495 return ERROR_NOT_SUPPORTED; // Cannot seek stdin
1496 }
1497
1498 // Flush codec buffers
1499 if (decoder->video_codec_ctx) {
1500 avcodec_flush_buffers(decoder->video_codec_ctx);
1501 }
1502 if (decoder->audio_codec_ctx) {
1503 avcodec_flush_buffers(decoder->audio_codec_ctx);
1504 }
1505
1506 // Seek to beginning
1507 if (av_seek_frame(decoder->format_ctx, -1, 0, AVSEEK_FLAG_BACKWARD) < 0) {
1508 return SET_ERRNO(ERROR_MEDIA_SEEK, "Failed to seek to beginning");
1509 }
1510
1511 decoder->eof_reached = false;
1512 decoder->video_draining = false;
1513 decoder->audio_buffer_offset = 0;
1514 decoder->last_video_pts = -1.0;
1515 decoder->last_audio_pts = -1.0;
1516 decoder->audio_samples_read = 0; // Reset sample counter to 0
1517
1518 return ASCIICHAT_OK;
1519}
1520
1522 if (!decoder) {
1523 return ERROR_INVALID_PARAM;
1524 }
1525
1526 if (decoder->is_stdin) {
1527 return ERROR_NOT_SUPPORTED; // Cannot seek stdin
1528 }
1529
1530 // Hold mutex during entire seek operation to prevent race with prefetch thread
1531 mutex_lock(&decoder->prefetch_mutex);
1532
1533 // Set flag to pause prefetch thread via condition variable
1534 decoder->seeking_in_progress = true;
1535
1536 // Convert seconds to FFmpeg time base units (AV_TIME_BASE = 1,000,000)
1537 int64_t target_ts = (int64_t)(timestamp_sec * AV_TIME_BASE);
1538
1539 // For HTTP streams, use simple keyframe seeking (faster than frame-accurate seeking)
1540 // HTTP seeking is expensive and can break stream state, so prefer speed over precision
1541 int seek_ret = av_seek_frame(decoder->format_ctx, -1, target_ts, AVSEEK_FLAG_BACKWARD);
1542 if (seek_ret < 0) {
1543 // Fallback: try without backward flag
1544 seek_ret = av_seek_frame(decoder->format_ctx, -1, target_ts, 0);
1545 }
1546
1547 if (seek_ret < 0) {
1548 decoder->seeking_in_progress = false;
1549 cond_signal(&decoder->prefetch_cond);
1550 mutex_unlock(&decoder->prefetch_mutex);
1551 return SET_ERRNO(ERROR_MEDIA_SEEK, "Failed to seek to timestamp %.2f seconds", timestamp_sec);
1552 }
1553
1554 // Flush codec buffers AFTER seeking
1555 if (decoder->video_codec_ctx) {
1556 avcodec_flush_buffers(decoder->video_codec_ctx);
1557 }
1558 if (decoder->audio_codec_ctx) {
1559 avcodec_flush_buffers(decoder->audio_codec_ctx);
1560 }
1561
1562 // Reset state
1563 decoder->eof_reached = false;
1564 decoder->video_draining = false;
1565 decoder->audio_buffer_offset = 0;
1566 // Clear any stale audio data in buffer
1567 if (decoder->audio_buffer) {
1568 memset(decoder->audio_buffer, 0, decoder->audio_buffer_size * sizeof(float));
1569 }
1570 decoder->last_video_pts = -1.0;
1571 decoder->last_audio_pts = -1.0;
1572 decoder->prefetch_frame_ready = false;
1573 // Set audio_samples_read to match the seek target so position tracking works correctly
1574 decoder->audio_samples_read = (uint64_t)(timestamp_sec * decoder->audio_sample_rate);
1575
1576 // Reset current_read_buffer and mark both buffers as not in use
1577 // After seeking, the prefetch thread may reallocate buffers, so current_read_buffer
1578 // could point to freed memory. Clear it so the next read doesn't try to release stale pointers.
1579 decoder->current_read_buffer = NULL;
1580 decoder->buffer_a_in_use = false;
1581 decoder->buffer_b_in_use = false;
1582
1583 // Resume prefetch thread
1584 decoder->seeking_in_progress = false;
1585 cond_signal(&decoder->prefetch_cond);
1586
1587 // Release mutex - prefetch thread can resume
1588 mutex_unlock(&decoder->prefetch_mutex);
1589
1590 return ASCIICHAT_OK;
1591}
1592
1594 return decoder && decoder->eof_reached;
1595}
1596
1598 if (!decoder || !decoder->format_ctx) {
1599 return -1.0;
1600 }
1601
1602 if (decoder->format_ctx->duration == AV_NOPTS_VALUE) {
1603 return -1.0;
1604 }
1605
1606 return (double)decoder->format_ctx->duration / AV_TIME_BASE;
1607}
1608
1610 if (!decoder) {
1611 return -1.0;
1612 }
1613
1614 // Prefer sample-based position tracking (continuous, works before frames are decoded)
1615 if (decoder->audio_sample_rate > 0 && decoder->audio_samples_read >= 0) {
1616 return (double)decoder->audio_samples_read / (double)decoder->audio_sample_rate;
1617 }
1618
1619 // Fallback to frame-based position if available
1620 if (decoder->last_video_pts >= 0.0) {
1621 return decoder->last_video_pts;
1622 } else if (decoder->last_audio_pts >= 0.0) {
1623 return decoder->last_audio_pts;
1624 }
1625
1626 return -1.0;
1627}
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Named object registry for debugging — log identifiable resource names.
🎞️ FFmpeg-based media decoder for video and audio streams
#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
unsigned char uint8_t
Definition common.h:56
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
#define NAMED_REGISTER_FFMPEG_DECODER(decoder, name, parent_ptr)
Register an FFmpeg decoder 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_MEDIA_SEEK
Definition error_codes.h:73
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_MEDIA_DECODE
Definition error_codes.h:72
@ ERROR_INVALID_PARAM
@ ERROR_MEDIA_OPEN
Definition error_codes.h:71
@ ERROR_THREAD
@ 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.
void ffmpeg_decoder_stop_prefetch(ffmpeg_decoder_t *decoder)
Stop the background frame prefetching thread.
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.
asciichat_error_t ffmpeg_decoder_rewind(ffmpeg_decoder_t *decoder)
Seek to beginning of media.
asciichat_error_t ffmpeg_decoder_get_video_dimensions(ffmpeg_decoder_t *decoder, int *width, int *height)
Get video dimensions.
bool ffmpeg_decoder_has_audio(ffmpeg_decoder_t *decoder)
Check if decoder has audio stream.
bool ffmpeg_decoder_is_prefetch_running(ffmpeg_decoder_t *decoder)
Check if background frame prefetching thread is running.
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)
Seek to specific timestamp in media.
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.
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.
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_INT
Definition time.h:156
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)
int asciichat_thread_join_timeout(asciichat_thread_t *thread, void **retval, uint64_t timeout_ns)
Wait for a thread to complete with timeout.
#define cond_wait(cond, mutex)
Wait on a condition variable (with debug tracking in debug builds)
Definition cond.h:275
#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
int cond_signal(cond_t *cond)
Signal a condition variable (wake one waiting thread)
int cond_init(cond_t *cond, const char *name)
Initialize a condition variable with a name.
void platform_sleep_us(unsigned int us)
High-precision sleep function with microsecond precision.
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
#define EAGAIN
int cond_destroy(cond_t *cond)
Destroy a condition variable.
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
bool url_is_valid(const char *url)
Fast URL validation using production-grade regex (HTTP/HTTPS/WebSocket/TCP)
Definition url.c:81
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.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
🧵 Cross-platform thread interface for ascii-chat
Capture stdout/stderr and redirect to logging system.
#define LOG_IO(prefix, block)
Capture output from a code block and log it.
Definition io.h:81
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)
void * asciichat_thread_t
#define asciichat_thread_join(thread, timeout_ms)
📝 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
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:699
#define AVIO_BUFFER_SIZE
#define TARGET_SAMPLE_RATE
atomic_t should_stop
Definition splash.c:97
int frame
Definition splash.c:99
#define bool
Definition stdbool.h:61
Condition variable type (POSIX: pthread_cond_t with debug tracking)
Definition cond.h:63
FFmpeg decoder state for video and audio decoding.
mutex_t prefetch_mutex
Protect prefetch state and FFmpeg decoder access.
uint64_t audio_samples_read
Total audio samples decoded and output.
AVCodecContext * video_codec_ctx
Video codec context.
image_t * prefetch_image_a
First prefetch buffer.
AVFormatContext * format_ctx
FFmpeg format/container context.
image_t * current_read_buffer
Buffer main thread is currently reading/rendering.
bool prefetch_thread_running
Whether prefetch thread is active.
size_t audio_buffer_offset
Current offset in audio buffer.
bool buffer_b_in_use
Whether prefetch_image_b is being read by main thread.
image_t * prefetch_image_b
Second prefetch buffer.
cond_t prefetch_cond
Condition variable for pausing during seek.
AVIOContext * avio_ctx
Custom I/O context for stdin.
bool video_draining
Whether EOF was sent to the video decoder.
mutex_t read_frame_mutex
Protect av_read_frame() calls from video and audio threads.
struct SwrContext * swr_ctx
Software resampler for format conversion.
bool prefetch_frame_ready
Whether current_prefetch_image has valid data.
AVPacket * packet
Reusable packet for reading.
int video_stream_idx
Video stream index (-1 if none)
bool eof_reached
Whether end of file was reached.
unsigned char * avio_buffer
Buffer for custom I/O.
double last_audio_pts
Last audio presentation timestamp.
bool seeking_in_progress
Signal to pause prefetch thread during seek.
AVCodecContext * audio_codec_ctx
Audio codec context.
bool is_stdin
Whether reading from stdin.
int audio_sample_rate
Audio sample rate (Hz)
size_t audio_buffer_size
Total size of audio buffer.
image_t * current_prefetch_image
Currently available prefetched frame.
bool buffer_a_in_use
Whether prefetch_image_a is being read by main thread.
float * audio_buffer
Buffer for partial audio frames.
double last_video_pts
Last video presentation timestamp.
bool prefetch_should_stop
Signal to stop prefetch thread.
asciichat_thread_t prefetch_thread
Prefetch thread handle.
bool(* should_exit_callback)(void *user_data)
Callback to check if app should exit.
void * exit_callback_user_data
User data for exit callback.
int audio_stream_idx
Audio stream index (-1 if none)
AVFrame * frame
Reusable frame for decoding.
image_t * current_image
Working buffer for decoding.
struct SwsContext * sws_ctx
Software scaler for format conversion.
Image structure.
int w
Image width in pixels (must be > 0)
int h
Image height in pixels (must be > 0)
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order)
Mutex type (POSIX: pthread_mutex_t with debug tracking)
RGB pixel structure.
size_t pos
Current read position (per-decoder)
stdin_buffer_t * buffer
Pointer to shared buffer.
uint8_t * data
Buffered data (shared between decoders)
size_t size
Total size of buffered data.
Cross-platform system functions interface for ascii-chat.
⏱️ High-precision timing utilities using sokol_time.h and uthash
🌐 Production-Grade URL Validation and Parsing