ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
ffmpeg_decoder.c
Go to the documentation of this file.
1
5
6/* Storage capacities in elements; text buffers include the terminating NUL. */
7#define MEDIA_FFMPEG_DECODER_DECODER_NAME_BUFFER_SIZE 256
8
9#include <ascii-chat/otel.h>
11#include <ascii-chat/common.h>
12#include <libplatform/atomic.h>
13#include <ascii-chat/log/log.h>
14#include <ascii-chat/log/io.h>
18#include <libplatform/system.h>
19#include <libplatform/util/string.h>
20#include <libplatform/mutex.h>
21#include <libplatform/cond.h>
22#include <libplatform/thread.h>
24#include <ascii-chat/util/url.h>
26
27#include <libavformat/avformat.h>
28#include <libavcodec/avcodec.h>
29#include <libavutil/imgutils.h>
30#include <libavutil/opt.h>
31#include <libavutil/log.h>
32#include <libswscale/swscale.h>
33#include <libswresample/swresample.h>
34#include <string.h>
35#include <inttypes.h>
36
37/* ============================================================================
38 * FFmpeg Logging Suppression
39 * ============================================================================ */
40
50static void ffmpeg_silent_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
51 (void)avcl; // unused
52 (void)level; // unused
53 (void)fmt; // unused
54 (void)vl; // unused
55 // Do nothing - silently discard all FFmpeg logs
56}
57
58/* ============================================================================
59 * Constants
60 * ============================================================================ */
61
63#define TARGET_SAMPLE_RATE 48000
64
66#define TARGET_CHANNELS 2
67
69#define AVIO_BUFFER_SIZE (64 * 1024)
70
71/* ============================================================================
72 * FFmpeg Decoder Structure
73 * ============================================================================ */
74
82 // Format context
83 AVFormatContext *format_ctx;
84
85 // Video stream
86 AVCodecContext *video_codec_ctx;
88 struct SwsContext *sws_ctx;
89
90 // Audio stream
91 AVCodecContext *audio_codec_ctx;
93 struct SwrContext *swr_ctx;
94
95 // Frame and packet buffers
96 AVFrame *frame;
97 AVPacket *packet;
98
99 // Decoded image cache - double-buffered for prefetching
101
102 // Audio sample buffer (for partial frame handling)
106
107 // Background thread for frame prefetching (reduces YouTube HTTP blocking)
108 platform_thread_t prefetch_thread;
112 _Atomic bool prefetch_frame_ready;
114 _Atomic bool prefetch_failed;
116 _Atomic bool prefetch_should_stop;
117 _Atomic bool seeking_in_progress;
122
123 // Track which buffer is being read by main thread
127
128 // State flags
129 atomic_t eof_reached;
130 bool is_stdin;
132
133 // Stdin I/O context
134 AVIOContext *avio_ctx;
135 unsigned char *avio_buffer;
136
137 // Position tracking
140
141 // Sample-based position tracking
144
145 // Exit signal callback (for graceful shutdown during blocking I/O)
146 atomic_ptr_t should_exit_callback;
148};
149
150/* ============================================================================
151 * Stdin I/O Callbacks
152 * ============================================================================ */
153
154/* ============================================================================
155 * Memory-based AVIO for stdin buffering
156 * ============================================================================ */
157
161typedef struct {
163 size_t size;
165
173
177static int memory_read_packet(void *opaque, uint8_t *buf, int buf_size) {
179
180 if (!reader || !reader->buffer || reader->pos >= reader->buffer->size)
181 return AVERROR_EOF;
182
183 int to_read = buf_size;
184 if (reader->pos + to_read > reader->buffer->size)
185 to_read = (int)(reader->buffer->size - reader->pos);
186
187 memcpy(buf, reader->buffer->data + reader->pos, to_read);
188 reader->pos += to_read;
189
190 return to_read;
191}
192
196static int64_t memory_seek_packet(void *opaque, int64_t offset, int whence) {
198
199 if (!reader || !reader->buffer)
200 return -1;
201
202 int64_t new_pos = 0;
203 if (whence == SEEK_SET) {
204 new_pos = offset;
205 } else if (whence == SEEK_CUR) {
206 new_pos = (int64_t)reader->pos + offset;
207 } else if (whence == SEEK_END) {
208 new_pos = (int64_t)reader->buffer->size + offset;
209 } else {
210 return -1;
211 }
212
213 if (new_pos < 0 || new_pos > (int64_t)reader->buffer->size)
214 return -1;
215
216 reader->pos = (size_t)new_pos;
217 return new_pos;
218}
219
224static stdin_buffer_t *stdin_buffer_read_all(void) {
226 if (!sb)
227 return NULL;
228
229 sb->data = NULL;
230 sb->size = 0;
231
232 // Read stdin in 64KB chunks
233 const size_t chunk_size = 65536;
234 uint8_t chunk_buffer[chunk_size];
235 size_t total_capacity = 0;
236 int chunk_count = 0;
237
238 while (1) {
239 size_t bytes_read = fread(chunk_buffer, 1, chunk_size, stdin);
240 if (bytes_read == 0)
241 break;
242 chunk_count++;
243
244 // Grow buffer if needed
245 if (sb->size + bytes_read > total_capacity) {
246 total_capacity = (sb->size + bytes_read) * 2; // Always double capacity
247 uint8_t *new_data = SAFE_MALLOC(total_capacity, uint8_t *);
248 if (!new_data) {
249 SAFE_FREE(sb->data);
250 SAFE_FREE(sb);
251 return NULL;
252 }
253 if (sb->data) {
254 memcpy(new_data, sb->data, sb->size);
255 SAFE_FREE(sb->data);
256 }
257 sb->data = new_data;
258 }
259
260 // Copy chunk to buffer
261 memcpy(sb->data + sb->size, chunk_buffer, bytes_read);
262 sb->size += bytes_read;
263 }
264
265 log_debug("Buffered stdin: %zu bytes in %d chunks (capacity: %zu)", sb->size, chunk_count, total_capacity);
266 if (sb->size > 0) {
267 log_info(
268 " First 16 bytes of stdin: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x",
269 sb->data[0], sb->data[1], sb->data[2], sb->data[3], sb->data[4], sb->data[5], sb->data[6], sb->data[7],
270 sb->data[8], sb->data[9], sb->data[10], sb->data[11], sb->data[12], sb->data[13], sb->data[14], sb->data[15]);
271 }
272 return sb;
273}
274
278static stdin_buffer_t *g_stdin_buffer = NULL;
279
280/* ============================================================================
281 * Helper Functions
282 * ============================================================================ */
283
287static inline double av_q2d_safe(AVRational r) {
288 return (r.den != 0) ? ((double)r.num / (double)r.den) : 0.0;
289}
290
294static double get_frame_pts_seconds(AVFrame *frame, AVRational time_base) {
295 if (frame->pts == AV_NOPTS_VALUE) {
296 return -1.0;
297 }
298 return (double)frame->pts * av_q2d_safe(time_base);
299}
300
307static int ffmpeg_interrupt_callback(void *opaque) {
308 ffmpeg_decoder_t *decoder = (ffmpeg_decoder_t *)opaque;
309 if (!decoder) {
310 return 0;
311 }
312
313 // Interrupt traced_packet_read() if a seek is in progress
314 if (decoder->seeking_in_progress) {
315 return 1;
316 }
317
318 // Interrupt traced_packet_read() if application is shutting down
319 // This allows Ctrl+C to abort blocking I/O operations like YouTube HTTP requests
320 bool (*cancel)(void *) = (bool (*)(void *))atomic_ptr_load(&decoder->should_exit_callback);
321 void *data = atomic_ptr_load(&decoder->exit_callback_user_data);
322 if (cancel && cancel(data)) {
323 return 1;
324 }
325
326 return 0;
327}
328
336
337static void ffmpeg_otel_result(int result, bool streaming) {
338 // EOF, buffering and application cancellation are expected control flow.
339 if (result < 0 && result != AVERROR_EXIT && !(streaming && (result == AVERROR_EOF || result == AVERROR(EAGAIN))))
340 gotel_current_error("ffmpeg.error", "FFmpeg operation failed");
341}
342
343static int traced_format_open(AVFormatContext **format, const char *path, const AVInputFormat *input,
344 AVDictionary **options) {
345 ASCII_OTEL_SCOPE(FFMPEG_FORMAT_OPEN);
346 ASCII_OTEL_ATTR_STRING("media.source.kind", !path ? "stdin" : strstr(path, "://") ? "network" : "file");
347 int result = avformat_open_input(format, path, input, options);
348 ASCII_OTEL_ATTR_INT("ffmpeg.result", result);
349 ffmpeg_otel_result(result, false);
350 return result;
351}
352static int traced_format_probe(AVFormatContext *format, AVDictionary **options) {
353 ASCII_OTEL_SCOPE(FFMPEG_PROBE);
354 int result = avformat_find_stream_info(format, options);
355 ASCII_OTEL_ATTR_INT("ffmpeg.result", result);
356 if (result >= 0 && format->iformat)
357 ASCII_OTEL_ATTR_STRING("media.container", format->iformat->name);
358 ffmpeg_otel_result(result, false);
359 return result;
360}
361static int traced_packet_read(AVFormatContext *format, AVPacket *packet) {
362 ASCII_OTEL_SCOPE(FFMPEG_PACKET_READ);
363 int result = av_read_frame(format, packet);
364 ASCII_OTEL_ATTR_INT("ffmpeg.result", result);
365 if (result >= 0)
366 ASCII_OTEL_ATTR_INT("media.packet.size", packet->size);
367 ffmpeg_otel_result(result, true);
368 return result;
369}
370static int traced_packet_send(AVCodecContext *codec, const AVPacket *packet) {
371 ASCII_OTEL_SCOPE(FFMPEG_PACKET_SEND);
372 ASCII_OTEL_ATTR_STRING("media.type", codec->codec_type == AVMEDIA_TYPE_AUDIO ? "audio" : "video");
373 ASCII_OTEL_ATTR_STRING("media.codec", avcodec_get_name(codec->codec_id));
374 ASCII_OTEL_ATTR_INT("media.decoder.draining", packet == NULL);
375 int result = avcodec_send_packet(codec, packet);
376 ASCII_OTEL_ATTR_INT("ffmpeg.result", result);
377 ffmpeg_otel_result(result, true);
378 return result;
379}
380static int traced_frame_receive(AVCodecContext *codec, AVFrame *frame) {
381 ASCII_OTEL_SCOPE(FFMPEG_FRAME_RECEIVE);
382 ASCII_OTEL_ATTR_STRING("media.type", codec->codec_type == AVMEDIA_TYPE_AUDIO ? "audio" : "video");
383 int result = avcodec_receive_frame(codec, frame);
384 ASCII_OTEL_ATTR_INT("ffmpeg.result", result);
385 ffmpeg_otel_result(result, true);
386 return result;
387}
388static int traced_video_scale(struct SwsContext *context, const uint8_t *const data[], const int stride[], int start,
389 int height, uint8_t *const output[], const int output_stride[]) {
390 ASCII_OTEL_SCOPE(FFMPEG_SCALE);
391 ASCII_OTEL_ATTR_INT("video.frame.height", height);
392 int result = sws_scale(context, data, stride, start, height, output, output_stride);
393 ASCII_OTEL_ATTR_INT("ffmpeg.result", result);
394 ffmpeg_otel_result(result, false);
395 return result;
396}
397static int traced_audio_resample(struct SwrContext *context, uint8_t **output, int capacity, const uint8_t **input,
398 int samples) {
399 ASCII_OTEL_SCOPE(FFMPEG_RESAMPLE);
400 ASCII_OTEL_ATTR_INT("audio.input.samples", samples);
401 int result = swr_convert(context, output, capacity, input, samples);
402 ASCII_OTEL_ATTR_INT("audio.output.samples", result > 0 ? result : 0);
403 ffmpeg_otel_result(result, false);
404 return result;
405}
406
407static void *ffmpeg_decoder_prefetch_thread_func(void *arg) {
408 ffmpeg_decoder_t *decoder = (ffmpeg_decoder_t *)arg;
409 if (!decoder || !decoder->prefetch_image_a || !decoder->prefetch_image_b) {
410 return NULL;
411 }
412
413 log_debug("Video prefetch thread started");
414 bool use_image_a = true; // Track which buffer we're using (critical for correct buffer swapping)
415
416 mutex_lock(&decoder->prefetch_mutex);
417 while (true) {
418
419 // The consumer releases the previous image and wakes us when taking the
420 // next frame. Seek completion and shutdown also signal this condition.
421 while (!decoder->prefetch_should_stop &&
422 (atomic_load_bool(&decoder->eof_reached) || decoder->seeking_in_progress || decoder->prefetch_frame_ready ||
423 (decoder->buffer_a_in_use && decoder->buffer_b_in_use)))
424 cond_wait(&decoder->prefetch_cond, &decoder->prefetch_mutex);
425 if (decoder->prefetch_should_stop) {
426 break;
427 }
428
429 use_image_a = !decoder->buffer_a_in_use;
430 uint64_t generation = decoder->seek_generation;
431 uint64_t read_start_ns = time_get_ns();
432 bool frame_decoded = false;
433 bool reached_eof = false;
434 bool packet_rejected = false;
436
437 // Release prefetch mutex but hold read_frame_mutex to prevent concurrent traced_packet_read() calls from audio
438 // thread
439 mutex_unlock(&decoder->prefetch_mutex);
440 mutex_lock(&decoder->read_frame_mutex);
441 if (generation != decoder->seek_generation || decoder->seeking_in_progress) {
442 mutex_unlock(&decoder->read_frame_mutex);
443 mutex_lock(&decoder->prefetch_mutex);
444 continue;
445 }
446 // Interrupted reads are expected. Publish a failure only after deciding
447 // whether this attempt was cancelled or superseded by a seek.
448 bool notifications_suppressed = errno_notifications_suppress(true);
449
450 ASCII_OTEL_SCOPE(FFMPEG_VIDEO_DECODE);
451 ASCII_OTEL_ATTR_STRING("media.codec", avcodec_get_name(decoder->video_codec_ctx->codec_id));
452 int decode_result = 0;
453 // Read packets until we get a video frame - read_frame_mutex HELD
454 while (true) {
455 int ret;
456 if (decoder->video_draining) {
457 ret = decode_result = traced_frame_receive(decoder->video_codec_ctx, decoder->frame);
458 if (ret == AVERROR_EOF) {
459 atomic_store_bool(&decoder->eof_reached, true);
460 reached_eof = true;
461 break;
462 }
463 if (ret < 0) {
464 break;
465 }
466 } else {
467 ret = decode_result = traced_packet_read(decoder->format_ctx, decoder->packet);
468 if (ret < 0) {
469 if (ret != AVERROR_EOF) {
470 break;
471 }
472
473 ret = decode_result = traced_packet_send(decoder->video_codec_ctx, NULL);
474 decoder->video_draining = true;
475 if (ret < 0 && ret != AVERROR_EOF) {
476 break;
477 }
478
479 ret = decode_result = traced_frame_receive(decoder->video_codec_ctx, decoder->frame);
480 if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN)) {
481 atomic_store_bool(&decoder->eof_reached, true);
482 reached_eof = true;
483 break;
484 }
485 if (ret < 0) {
486 break;
487 }
488 } else {
489 // Check if this is a video packet
490 if (decoder->packet->stream_index != decoder->video_stream_idx) {
491 av_packet_unref(decoder->packet);
492 continue;
493 }
494
495 // Send packet to decoder
496 ret = decode_result = traced_packet_send(decoder->video_codec_ctx, decoder->packet);
497 av_packet_unref(decoder->packet);
498
499 if (ret == AVERROR(EAGAIN))
500 continue;
501 if (ret < 0) {
502 packet_rejected = true;
503 break;
504 }
505
506 // Receive decoded frame
507 ret = decode_result = traced_frame_receive(decoder->video_codec_ctx, decoder->frame);
508 if (ret == AVERROR(EAGAIN)) {
509 continue; // Need more packets
510 } else if (ret < 0) {
511 break;
512 }
513 }
514 }
515
516 // Frame decoded - mutex still held
517 // Update position tracking
518 decoder->last_video_pts =
519 get_frame_pts_seconds(decoder->frame, decoder->format_ctx->streams[decoder->video_stream_idx]->time_base);
520
521 // Convert frame to RGB24
522 int width = decoder->video_codec_ctx->width;
523 ASCII_OTEL_ATTR_INT("video.frame.width", width);
524 ASCII_OTEL_ATTR_INT("video.frame.height", decoder->video_codec_ctx->height);
525 int height = decoder->video_codec_ctx->height;
526
527 // Get current decode buffer based on which one we're using
528 // (Buffer availability was already checked before entering the decode loop)
529 image_t *decode_buffer = use_image_a ? decoder->prefetch_image_a : decoder->prefetch_image_b;
530
531 // Reallocate if needed (width/height changed)
532 if (decode_buffer->w != width || decode_buffer->h != height) {
533 image_t *replacement = image_new((size_t)width, (size_t)height);
534 if (!replacement) {
535 SET_ERRNO(ERROR_MEMORY, "Failed to allocate prefetch image buffer");
536 break;
537 }
538 image_destroy(decode_buffer);
539 decode_buffer = replacement;
540 // Update decoder's pointer to the reallocated buffer
541 if (use_image_a) {
542 decoder->prefetch_image_a = decode_buffer;
543 } else {
544 decoder->prefetch_image_b = decode_buffer;
545 }
546 }
547
548 // Convert pixel format (rgb_pixel_t is 3 bytes: r, g, b)
549 uint8_t *dst_data[1] = {(uint8_t *)decode_buffer->pixels};
550 int dst_linesize[1] = {width * 3};
551
552 static int decode_count = 0;
553 if (++decode_count <= 3) {
554 log_debug("[FRAME_DECODE] Frame %d: width=%d, height=%d, codec_pix_fmt=%d, frame_pix_fmt=%d (is_stdin=%d)",
555 decode_count, width, height, decoder->video_codec_ctx->pix_fmt, decoder->frame->format,
556 decoder->is_stdin);
557 }
558
559 // For fragmented MP4 from stdin, pixel format might not be set - default to YUV420p
560 // which is standard for H.264/H.265 video codecs
561 int pix_fmt_to_use = decoder->video_codec_ctx->pix_fmt;
562 if (pix_fmt_to_use == AV_PIX_FMT_NONE) {
563 if (decoder->frame->format != AV_PIX_FMT_NONE) {
564 pix_fmt_to_use = decoder->frame->format;
565 log_debug("[SWSCALE_FIX] Using frame->format=%d instead of codec pix_fmt (was NONE)", decoder->frame->format);
566 } else {
567 // Fragmented MP4: Default to YUV420p (standard for H.264/H.265)
568 pix_fmt_to_use = AV_PIX_FMT_YUV420P;
569 log_debug("[SWSCALE_FIX] Fragmented format: Using hardcoded YUV420p (frame->format was also NONE)");
570 }
571 }
572
573 // Lazy initialize swscale context if not done at startup (happens with HTTP/stdin streams)
574 if (!decoder->sws_ctx) {
575 log_debug("Lazy init swscale: width=%d, height=%d, pix_fmt=%d", width, height, pix_fmt_to_use);
576 if (width > 0 && height > 0 && pix_fmt_to_use != AV_PIX_FMT_NONE) {
577 LOG_IO("swscaler", {
578 decoder->sws_ctx = sws_getContext(width, height, pix_fmt_to_use, width, height, AV_PIX_FMT_RGB24,
579 SWS_BILINEAR, NULL, NULL, NULL);
580 });
581 if (!decoder->sws_ctx) {
583 "Failed to create swscale context: pix_fmt=%d (might be unsupported for conversion)",
584 pix_fmt_to_use);
585 break;
586 }
587
588 // Set proper colorspace for H.264 video (BT.709)
589 // sws_setColorspaceDetails(ctx, inv_table, srcRange, table, dstRange, brightness, contrast, saturation)
590 const int *inv_table = sws_getCoefficients(SWS_CS_ITU709);
591 const int *table = sws_getCoefficients(SWS_CS_ITU709);
592 int ret_cs = sws_setColorspaceDetails(decoder->sws_ctx, inv_table, 0, table, 0, 0, 1 << 16, 1 << 16);
593 if (ret_cs < 0) {
594 log_warn("Failed to set BT.709 colorspace details (code=%d), continuing with defaults", ret_cs);
595 } else {
596 log_debug("Set BT.709 colorspace for H.264 YUV->RGB conversion");
597 }
598
599 log_debug("Lazy initialized swscale context with %dx%d from pix_fmt=%d", width, height, pix_fmt_to_use);
600 } else {
602 "Cannot initialize swscale: width=%d, height=%d, pix_fmt=%d (AV_PIX_FMT_NONE=%d)", width, height,
603 pix_fmt_to_use, AV_PIX_FMT_NONE);
604 break;
605 }
606 }
607
608 if (decode_count <= 3) {
609 // Log input YUV data
610 uint8_t *y_data = decoder->frame->data[0];
611 uint8_t *u_data = decoder->frame->data[1];
612 uint8_t *v_data = decoder->frame->data[2];
613 log_debug("[YUV_INPUT] Frame %d: Y[0]=%u, U[0]=%u, V[0]=%u (linesize=%d,%d,%d)", decode_count,
614 y_data ? y_data[0] : 255, u_data ? u_data[0] : 255, v_data ? v_data[0] : 255,
615 decoder->frame->linesize[0], decoder->frame->linesize[1], decoder->frame->linesize[2]);
616 }
617
618 int ret_scale = traced_video_scale(decoder->sws_ctx, (const uint8_t *const *)decoder->frame->data,
619 decoder->frame->linesize, 0, height, dst_data, dst_linesize);
620 if (ret_scale <= 0) {
621 SET_ERRNO(ERROR_MEDIA_DECODE, "Video conversion failed: %d", ret_scale);
622 break;
623 }
624 if (decode_count <= 3) {
625 log_debug("[SWS_SCALE] Frame %d: returned %d", decode_count, ret_scale);
626 rgb_pixel_t *test_px = (rgb_pixel_t *)decode_buffer->pixels;
627 log_debug("[SWS_RESULT] Frame %d: RGB[0]=(%u,%u,%u)", decode_count, test_px[0].r, test_px[0].g, test_px[0].b);
628 }
629
630 frame_decoded = true;
631 break; // Exit while loop
632 }
633
634 // Release read_frame_mutex now that traced_packet_read() is done
635 mutex_unlock(&decoder->read_frame_mutex);
636
637 // Shutdown can race a buffered read without FFmpeg returning AVERROR_EXIT.
638 // The caller's cancellation request is authoritative for this operation.
639 bool (*cancel)(void *) = (bool (*)(void *))atomic_ptr_load(&decoder->should_exit_callback);
640 bool cancelled = cancel && cancel(atomic_ptr_load(&decoder->exit_callback_user_data));
641
642 if (frame_decoded) {
643 char read_time_str[TIME_DURATION_BUFFER_SIZE];
644 time_pretty(time_elapsed_ns(read_start_ns, time_get_ns()), -1, read_time_str, sizeof(read_time_str));
645 log_dev_every(5 * US_PER_SEC_INT, "PREFETCH: decoded frame in %s", read_time_str);
646 }
647
648 // Re-acquire mutex to update prefetch state
649 mutex_lock(&decoder->prefetch_mutex);
650
651 // Mutex now held - update prefetch state
652 bool obsolete = generation != decoder->seek_generation;
653 bool interrupted = decoder->prefetch_should_stop || decoder->seeking_in_progress || obsolete || cancelled;
654 errno_notifications_suppress(notifications_suppressed);
655 if (interrupted)
657 if (obsolete) {
658 frame_decoded = false;
659 reached_eof = false;
660 }
661 if (!frame_decoded && !reached_eof && !interrupted) {
662 if (!errno_peek_since(decode_scope, &decoder->prefetch_failure)) {
663 if (packet_rejected)
664 SET_ERRNO(ERROR_MEDIA_DECODE, "Video decoder rejected packet: %d", decode_result);
665 else
666 SET_ERRNO(ERROR_MEDIA_DECODE, "Background video decoding failed (FFmpeg result %d)", decode_result);
667 if (!errno_peek_since(decode_scope, &decoder->prefetch_failure)) {
669 SAFE_STRNCPY(decoder->prefetch_failure.context_message, "Background video decoding failed",
670 sizeof(decoder->prefetch_failure.context_message));
671 }
672 }
673 }
674
675 if (!frame_decoded && !reached_eof && !interrupted)
676 atomic_store_explicit(&decoder->prefetch_failed, true, memory_order_release);
677
678 if (frame_decoded) {
679 // Get current decode buffer
680 image_t *decode_buffer = use_image_a ? decoder->prefetch_image_a : decoder->prefetch_image_b;
681
682 // Update the current prefetch image (main thread will pull from this)
683 decoder->current_prefetch_image = decode_buffer;
684 atomic_store_explicit(&decoder->prefetch_frame_ready, true, memory_order_release);
685
686 } else if (cancelled || (!reached_eof && !interrupted)) {
687 // A decoder error terminates prefetch; EOF waits for rewind above.
688 break;
689 }
690 }
691
692 mutex_unlock(&decoder->prefetch_mutex);
693 log_debug("Video prefetch thread stopped");
694 return NULL;
695}
696
700static asciichat_error_t open_codec_context(AVFormatContext *fmt_ctx, enum AVMediaType type, int *stream_idx,
701 AVCodecContext **codec_ctx) {
702 ASCII_OTEL_SCOPE(FFMPEG_CODEC_OPEN);
703 ASCII_OTEL_ATTR_STRING("media.type", type == AVMEDIA_TYPE_AUDIO ? "audio" : "video");
704 int ret = av_find_best_stream(fmt_ctx, type, -1, -1, NULL, 0);
705 if (ret < 0) {
706 // Stream not found - not an error, just means no stream of this type
707 *stream_idx = -1;
708 *codec_ctx = NULL;
709 return ASCIICHAT_OK;
710 }
711
712 *stream_idx = ret;
713 AVStream *stream = fmt_ctx->streams[ret];
714
715 // Find decoder
716 const AVCodec *codec = avcodec_find_decoder(stream->codecpar->codec_id);
717 if (!codec) {
718 return SET_ERRNO(ERROR_MEDIA_DECODE, "Codec not found for stream %d", ret);
719 }
720
721 ASCII_OTEL_ATTR_STRING("media.codec", codec->name);
722 // Allocate codec context
723 *codec_ctx = avcodec_alloc_context3(codec);
724 if (!*codec_ctx) {
725 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate codec context");
726 }
727
728 // Copy codec parameters
729 ret = avcodec_parameters_to_context(*codec_ctx, stream->codecpar);
730 if (ret < 0) {
731 avcodec_free_context(codec_ctx);
732 return SET_ERRNO(ret == AVERROR(ENOMEM) ? ERROR_MEMORY : ERROR_MEDIA_DECODE, "Failed to copy codec parameters");
733 }
734
735 // Open codec
736 ret = avcodec_open2(*codec_ctx, codec, NULL);
737 if (ret < 0) {
738 avcodec_free_context(codec_ctx);
739 return SET_ERRNO(ret == AVERROR(ENOMEM) ? ERROR_MEMORY : ERROR_MEDIA_DECODE, "Failed to open codec");
740 }
741
742 return ASCIICHAT_OK;
743}
744
745// A missing/unsupported stream can fall back to the other media type. Allocation
746// and other initialization failures still fail decoder creation with their cause.
747static asciichat_error_t open_optional_codec_context(AVFormatContext *format, enum AVMediaType type, int *stream_index,
748 AVCodecContext **context) {
750 asciichat_error_t result = open_codec_context(format, type, stream_index, context);
752 LOG_ERRNO_IF_SET("Optional media stream unavailable");
753 avcodec_free_context(context);
754 *stream_index = -1;
756 return ASCIICHAT_OK;
757 }
758 if (result != ASCIICHAT_OK)
759 return result;
762 return ASCIICHAT_OK;
763}
764
765/* ============================================================================
766 * Decoder Lifecycle
767 * ============================================================================ */
768
770 return ffmpeg_decoder_create_interruptible(path, NULL, NULL);
771}
772
773ffmpeg_decoder_t *ffmpeg_decoder_create_interruptible(const char *path, bool (*cancel)(void *), void *data) {
774 return ffmpeg_decoder_create_with_request_options(path, cancel, data, NULL, NULL, NULL);
775}
776
777ffmpeg_decoder_t *ffmpeg_decoder_create_with_request_options(const char *path, bool (*cancel)(void *), void *data,
778 const char *headers, const char *cookies,
779 const char *proxy) {
780 ASCII_OTEL_SCOPE(FFMPEG_DECODER_CREATE_INTERRUPTIBLE);
781 if (!path) {
782 SET_ERRNO(ERROR_INVALID_PARAM, "Path is NULL");
783 return NULL;
784 }
785
786 // Suppress FFmpeg's verbose debug logging (H.264 codec warnings, etc.)
787 // Only set this once, it's a global setting
788 static bool ffmpeg_log_level_set = false;
789 if (!ffmpeg_log_level_set) {
790 av_log_set_level(AV_LOG_QUIET); // Suppress all FFmpeg logging
791 av_log_set_callback(ffmpeg_silent_log_callback); // Install silent callback to discard all output
792 ffmpeg_log_level_set = true;
793 }
794
796 if (!decoder) {
797 SET_ERRNO(ERROR_MEMORY, "Failed to allocate decoder");
798 return NULL;
799 }
800
801 memset(decoder, 0, sizeof(*decoder));
802 atomic_ptr_store(&decoder->should_exit_callback, (void *)cancel);
803 atomic_ptr_store(&decoder->exit_callback_user_data, data);
804 decoder->format_ctx = avformat_alloc_context();
805 if (!decoder->format_ctx) {
806 SAFE_FREE(decoder);
807 return NULL;
808 }
809 decoder->format_ctx->interrupt_callback.callback = ffmpeg_interrupt_callback;
810 decoder->format_ctx->interrupt_callback.opaque = decoder;
811 decoder->video_stream_idx = -1;
812 decoder->audio_stream_idx = -1;
813 decoder->last_video_pts = -1.0;
814 decoder->last_audio_pts = -1.0;
815
816 // Capture FFmpeg's probing output (FFmpeg may write directly to either stream)
817 int ret = 0;
818 LOG_IO("ffmpeg", {
819 // Configure FFmpeg options for HTTP streaming performance
820 AVDictionary *options = NULL;
821
822 // FFmpeg accepts HTTP authorities that the application URL validator rejects.
823 // Classify the protocol rather than validating the host for timeout settings.
824 bool is_http = path && (platform_strncasecmp(path, "http://", sizeof("http://") - 1) == 0 ||
825 platform_strncasecmp(path, "https://", sizeof("https://") - 1) == 0);
827 bool is_url = is_http || (path && strstr(path, "://") && url_is_valid(path));
829 if (is_url) {
830 if (headers && *headers)
831 av_dict_set(&options, "headers", headers, 0);
832 if (cookies && *cookies)
833 av_dict_set(&options, "cookies", cookies, 0);
834 if (proxy)
835 av_dict_set(&options, "http_proxy", proxy, 0);
836 // Limit probing to 32KB for faster format detection
837 av_dict_set(&options, "probesize", "32768", 0);
838 // Analyze for 100ms max to determine streams quickly
839 av_dict_set(&options, "analyzeduration", "100000", 0);
840 // Enable auto-reconnection for interrupted connections
841 av_dict_set(&options, "reconnect", "1", 0);
842 // Allow reconnection for streamed protocols
843 av_dict_set(&options, "reconnect_streamed", "1", 0);
844 // Set reasonable I/O timeout (10 seconds)
845 av_dict_set(&options, "rw_timeout", "10000000", 0);
846 // Enable HTTP persistent connection (keep-alive) for better performance
847 av_dict_set(&options, "http_persistent", "1", 0);
848 // Reduce connect timeout to fail faster if server is unreachable
849 av_dict_set(&options, "connect_timeout", "5000000", 0);
850 }
851
852 // Open input file
853 ret = traced_format_open(&decoder->format_ctx, path, NULL, &options);
854 av_dict_free(&options); // Free options dictionary
855
856 if (ret >= 0) {
857 // Find stream info
858 if (traced_format_probe(decoder->format_ctx, NULL) < 0) {
859 ret = -1;
860 }
861 }
862 });
863
864 if (ret < 0) {
865 if (ret == -1) {
866 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to find stream info");
867 } else {
868 SET_ERRNO(ERROR_MEDIA_OPEN, "Failed to open media file: %s", path);
869 }
870 if (decoder->format_ctx) {
871 avformat_close_input(&decoder->format_ctx);
872 }
873 SAFE_FREE(decoder);
874 return NULL;
875 }
876
877 // Install interrupt callback to allow seeking to interrupt long traced_packet_read() calls
878 // Do this AFTER finding stream info to ensure format context is fully initialized
879 if (decoder->format_ctx) {
880 decoder->format_ctx->interrupt_callback.callback = ffmpeg_interrupt_callback;
881 decoder->format_ctx->interrupt_callback.opaque = decoder;
882 }
883
884 // Open video codec
885 asciichat_error_t err = open_optional_codec_context(decoder->format_ctx, AVMEDIA_TYPE_VIDEO,
886 &decoder->video_stream_idx, &decoder->video_codec_ctx);
887 if (err != ASCIICHAT_OK) {
888 ffmpeg_decoder_destroy(decoder);
889 return NULL;
890 }
891
892 // Open audio codec - audio is enabled by default (no option needed)
893 // Always try to open audio codec, don't rely on GET_OPTION(audio_enabled) which has a default issue
894 err = open_optional_codec_context(decoder->format_ctx, AVMEDIA_TYPE_AUDIO, &decoder->audio_stream_idx,
895 &decoder->audio_codec_ctx);
896 if (err != ASCIICHAT_OK) {
897 ffmpeg_decoder_destroy(decoder);
898 return NULL;
899 }
900
901 // Require at least one stream
902 if (decoder->video_stream_idx < 0 && decoder->audio_stream_idx < 0) {
903 SET_ERRNO(ERROR_MEDIA_DECODE, "No video or audio streams found");
904 ffmpeg_decoder_destroy(decoder);
905 return NULL;
906 }
907
908 // Allocate frame and packet
909 decoder->frame = av_frame_alloc();
910 decoder->packet = av_packet_alloc();
911 if (!decoder->frame || !decoder->packet) {
912 SET_ERRNO(ERROR_MEMORY, "Failed to allocate frame/packet");
913 ffmpeg_decoder_destroy(decoder);
914 return NULL;
915 }
916
917 // Initialize swscale context for video if present
918 if (decoder->video_codec_ctx) {
919 // Validate codec context has valid dimensions and pixel format
920 // For HTTP streams, these might not be valid until first frame is read
921 if (decoder->video_codec_ctx->width <= 0 || decoder->video_codec_ctx->height <= 0) {
922 log_warn("Video codec has invalid dimensions (%dx%d), will initialize swscale on first frame",
923 decoder->video_codec_ctx->width, decoder->video_codec_ctx->height);
924 // Don't create swscale context yet - will create it lazily on first frame read
925 } else if (decoder->video_codec_ctx->pix_fmt == AV_PIX_FMT_NONE) {
926 log_warn("Video codec has invalid pixel format, will initialize swscale on first frame");
927 // Don't create swscale context yet - will create it lazily on first frame read
928 } else {
929 // Create swscale context with valid parameters
930 LOG_IO("swscaler", {
931 decoder->sws_ctx =
932 sws_getContext(decoder->video_codec_ctx->width, decoder->video_codec_ctx->height,
933 decoder->video_codec_ctx->pix_fmt, decoder->video_codec_ctx->width,
934 decoder->video_codec_ctx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
935 });
936 if (!decoder->sws_ctx) {
937 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to create swscale context");
938 ffmpeg_decoder_destroy(decoder);
939 return NULL;
940 }
941 }
942 }
943
944 // Initialize swresample context for audio if present
945 if (decoder->audio_codec_ctx) {
946 // Store output sample rate for position tracking
948
949 // Allocate resampler context
950 decoder->swr_ctx = swr_alloc();
951 if (!decoder->swr_ctx) {
952 SET_ERRNO(ERROR_MEMORY, "Failed to allocate swresample context");
953 ffmpeg_decoder_destroy(decoder);
954 return NULL;
955 }
956
957 // Set options
958 av_opt_set_chlayout(decoder->swr_ctx, "in_chlayout", &decoder->audio_codec_ctx->ch_layout, 0);
959 av_opt_set_int(decoder->swr_ctx, "in_sample_rate", decoder->audio_codec_ctx->sample_rate, 0);
960 av_opt_set_sample_fmt(decoder->swr_ctx, "in_sample_fmt", decoder->audio_codec_ctx->sample_fmt, 0);
961
962 AVChannelLayout out_ch_layout = AV_CHANNEL_LAYOUT_STEREO;
963 av_opt_set_chlayout(decoder->swr_ctx, "out_chlayout", &out_ch_layout, 0);
964 av_opt_set_int(decoder->swr_ctx, "out_sample_rate", TARGET_SAMPLE_RATE, 0);
965 av_opt_set_sample_fmt(decoder->swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
966
967 // A mono source stays centered at unity gain in the stereo pipeline.
968 if (decoder->audio_codec_ctx->ch_layout.nb_channels == 1) {
969 const double matrix[] = {1.0, 1.0};
970 swr_set_matrix(decoder->swr_ctx, matrix, 1);
971 }
972 // Initialize
973 if (swr_init(decoder->swr_ctx) < 0) {
974 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to initialize swresample context");
975 ffmpeg_decoder_destroy(decoder);
976 return NULL;
977 }
978
979 // Allocate audio buffer (10 seconds worth)
981 decoder->audio_buffer = SAFE_MALLOC(decoder->audio_buffer_size * sizeof(float), float *);
982 if (!decoder->audio_buffer) {
983 SET_ERRNO(ERROR_MEMORY, "Failed to allocate audio buffer");
984 ffmpeg_decoder_destroy(decoder);
985 return NULL;
986 }
987 }
988
989 // Initialize video frame prefetching system (for YouTube streaming)
990 if (decoder->video_stream_idx >= 0) {
991 int width = decoder->video_codec_ctx->width;
992 int height = decoder->video_codec_ctx->height;
993
994 // Create two prefetch image buffers for double-buffering
995 decoder->prefetch_image_a = image_new((size_t)width, (size_t)height);
996 decoder->prefetch_image_b = image_new((size_t)width, (size_t)height);
997 if (!decoder->prefetch_image_a || !decoder->prefetch_image_b) {
998 SET_ERRNO(ERROR_MEMORY, "Failed to allocate prefetch image buffers");
999 ffmpeg_decoder_destroy(decoder);
1000 return NULL;
1001 }
1002
1003 decoder->current_prefetch_image = decoder->prefetch_image_a;
1004 decoder->prefetch_frame_ready = false;
1005
1006 // Initialize prefetch mutex
1007 if (mutex_init(&decoder->prefetch_mutex, "ffmpeg_prefetch") != 0) {
1008 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch mutex");
1009 ffmpeg_decoder_destroy(decoder);
1010 return NULL;
1011 }
1012
1013 if (cond_init(&decoder->prefetch_cond, "ffmpeg_prefetch") != 0) {
1014 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch condition variable");
1015 mutex_destroy(&decoder->prefetch_mutex);
1016 ffmpeg_decoder_destroy(decoder);
1017 return NULL;
1018 }
1019
1020 if (mutex_init(&decoder->read_frame_mutex, "ffmpeg_read_frame") != 0) {
1021 SET_ERRNO(ERROR_MEMORY, "Failed to initialize read_frame mutex");
1022 mutex_destroy(&decoder->prefetch_mutex);
1023 cond_destroy(&decoder->prefetch_cond);
1024 ffmpeg_decoder_destroy(decoder);
1025 return NULL;
1026 }
1027
1028 decoder->prefetch_thread_running = false;
1029 decoder->prefetch_should_stop = false;
1030 }
1031
1032 log_debug("FFmpeg decoder opened: %s (video=%s, audio=%s)", path, decoder->video_stream_idx >= 0 ? "yes" : "no",
1033 decoder->audio_stream_idx >= 0 ? "yes" : "no");
1034
1036 snprintf(decoder_name, sizeof(decoder_name), "%s", path);
1037 NAMED_REGISTER_FFMPEG_DECODER(decoder, decoder_name, NULL);
1038 NAMED_REGISTER_ATOMIC(&decoder->eof_reached, "ffmpeg_eof", decoder);
1039
1040 return decoder;
1041}
1042
1044 ASCII_OTEL_SCOPE(FFMPEG_DECODER_CREATE_STDIN);
1045 // Buffer entire stdin on first call (shared between video and audio decoders)
1046 if (!g_stdin_buffer) {
1047 log_debug("Reading stdin into buffer...");
1048 g_stdin_buffer = stdin_buffer_read_all();
1049 if (!g_stdin_buffer) {
1050 SET_ERRNO(ERROR_MEDIA_OPEN, "Failed to read stdin into buffer");
1051 return NULL;
1052 }
1053 }
1054
1056 if (!decoder) {
1057 SET_ERRNO(ERROR_MEMORY, "Failed to allocate decoder");
1058 return NULL;
1059 }
1060
1061 memset(decoder, 0, sizeof(*decoder));
1062 decoder->video_stream_idx = -1;
1063 decoder->audio_stream_idx = -1;
1064 decoder->is_stdin = true;
1065 decoder->last_video_pts = -1.0;
1066 decoder->last_audio_pts = -1.0;
1067
1068 // Create per-decoder reader (each decoder gets independent position tracking)
1070 if (!reader) {
1071 SET_ERRNO(ERROR_MEMORY, "Failed to allocate buffer reader");
1072 SAFE_FREE(decoder);
1073 return NULL;
1074 }
1075 reader->buffer = g_stdin_buffer;
1076 reader->pos = 0; // Each decoder starts from position 0
1077
1078 // AVIO takes ownership of this buffer and may resize or free it with av_free().
1079 decoder->avio_buffer = av_malloc(AVIO_BUFFER_SIZE);
1080 if (!decoder->avio_buffer) {
1081 SET_ERRNO(ERROR_MEMORY, "Failed to allocate AVIO buffer");
1082 SAFE_FREE(reader);
1083 SAFE_FREE(decoder);
1084 return NULL;
1085 }
1086
1087 decoder->avio_ctx = avio_alloc_context(decoder->avio_buffer, // internal buffer for AVIO
1088 AVIO_BUFFER_SIZE, // buffer size
1089 0, // write_flag
1090 reader, // opaque (per-decoder reader)
1091 memory_read_packet,
1092 NULL, // write_packet
1093 memory_seek_packet // seek (memory is seekable)
1094 );
1095
1096 if (!decoder->avio_ctx) {
1097 SET_ERRNO(ERROR_MEMORY, "Failed to create AVIO context");
1098 av_free(decoder->avio_buffer);
1099 decoder->avio_buffer = NULL;
1100 SAFE_FREE(decoder);
1101 return NULL;
1102 }
1103
1104 // Mark AVIO context as seekable (memory buffer supports seeking)
1105 decoder->avio_ctx->seekable = AVIO_SEEKABLE_NORMAL;
1106
1107 // Allocate format context
1108 decoder->format_ctx = avformat_alloc_context();
1109 if (!decoder->format_ctx) {
1110 SET_ERRNO(ERROR_MEMORY, "Failed to allocate format context");
1111 av_freep(&decoder->avio_ctx->buffer);
1112 avio_context_free(&decoder->avio_ctx);
1113 SAFE_FREE(decoder);
1114 return NULL;
1115 }
1116
1117 decoder->format_ctx->pb = decoder->avio_ctx;
1118 decoder->format_ctx->flags |= AVFMT_FLAG_CUSTOM_IO; // Tell FFmpeg to use our custom AVIO context
1119
1120 // Capture FFmpeg's probing output
1121 int ret = 0;
1122 LOG_IO("ffmpeg", {
1123 // Open input from buffered stdin
1124 ret = traced_format_open(&decoder->format_ctx, NULL, NULL, NULL);
1125 if (ret >= 0) {
1126 // Find stream info
1127 if (traced_format_probe(decoder->format_ctx, NULL) < 0) {
1128 ret = -1;
1129 }
1130 }
1131 });
1132
1133 if (ret < 0) {
1134 if (ret == -1) {
1135 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to find stream info from stdin buffer");
1136 } else {
1137 SET_ERRNO(ERROR_MEDIA_OPEN, "Failed to open stdin buffer");
1138 }
1139 av_freep(&decoder->avio_ctx->buffer);
1140 avio_context_free(&decoder->avio_ctx);
1141 if (decoder->format_ctx) {
1142 avformat_free_context(decoder->format_ctx);
1143 }
1144 SAFE_FREE(decoder);
1145 return NULL;
1146 }
1147
1148 // Open codecs (same as file-based decoder)
1149 asciichat_error_t err = open_optional_codec_context(decoder->format_ctx, AVMEDIA_TYPE_VIDEO,
1150 &decoder->video_stream_idx, &decoder->video_codec_ctx);
1151 if (err != ASCIICHAT_OK) {
1152 ffmpeg_decoder_destroy(decoder);
1153 return NULL;
1154 }
1155
1156 if (GET_OPTION(audio_enabled)) {
1157 err = open_optional_codec_context(decoder->format_ctx, AVMEDIA_TYPE_AUDIO, &decoder->audio_stream_idx,
1158 &decoder->audio_codec_ctx);
1159 if (err != ASCIICHAT_OK) {
1160 ffmpeg_decoder_destroy(decoder);
1161 return NULL;
1162 }
1163 } else {
1164 decoder->audio_stream_idx = -1;
1165 decoder->audio_codec_ctx = NULL;
1166 log_debug("Audio decoding disabled by user option");
1167 }
1168
1169 if (decoder->video_stream_idx < 0 && decoder->audio_stream_idx < 0) {
1170 SET_ERRNO(ERROR_MEDIA_DECODE, "No video or audio streams found in stdin");
1171 ffmpeg_decoder_destroy(decoder);
1172 return NULL;
1173 }
1174
1175 // Allocate frame and packet
1176 decoder->frame = av_frame_alloc();
1177 decoder->packet = av_packet_alloc();
1178 if (!decoder->frame || !decoder->packet) {
1179 SET_ERRNO(ERROR_MEMORY, "Failed to allocate frame/packet");
1180 ffmpeg_decoder_destroy(decoder);
1181 return NULL;
1182 }
1183
1184 // Initialize swscale/swresample (same as file-based)
1185 if (decoder->video_codec_ctx) {
1186 // Validate codec context has valid dimensions and pixel format
1187 // For stdin/HTTP streams, these might not be valid until first frame is read
1188 if (decoder->video_codec_ctx->width <= 0 || decoder->video_codec_ctx->height <= 0) {
1189 log_warn("Video codec has invalid dimensions (%dx%d), will initialize swscale on first frame",
1190 decoder->video_codec_ctx->width, decoder->video_codec_ctx->height);
1191 // Don't create swscale context yet - will create it lazily on first frame read
1192 } else if (decoder->video_codec_ctx->pix_fmt == AV_PIX_FMT_NONE) {
1193 log_warn("Video codec has invalid pixel format, will initialize swscale on first frame");
1194 // Don't create swscale context yet - will create it lazily on first frame read
1195 } else {
1196 // Create swscale context with valid parameters
1197 LOG_IO("swscaler", {
1198 decoder->sws_ctx =
1199 sws_getContext(decoder->video_codec_ctx->width, decoder->video_codec_ctx->height,
1200 decoder->video_codec_ctx->pix_fmt, decoder->video_codec_ctx->width,
1201 decoder->video_codec_ctx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
1202 });
1203 if (!decoder->sws_ctx) {
1204 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to create swscale context");
1205 ffmpeg_decoder_destroy(decoder);
1206 return NULL;
1207 }
1208 }
1209 }
1210
1211 if (decoder->audio_codec_ctx) {
1212 decoder->swr_ctx = swr_alloc();
1213 if (!decoder->swr_ctx) {
1214 SET_ERRNO(ERROR_MEMORY, "Failed to allocate swresample context");
1215 ffmpeg_decoder_destroy(decoder);
1216 return NULL;
1217 }
1218
1219 av_opt_set_chlayout(decoder->swr_ctx, "in_chlayout", &decoder->audio_codec_ctx->ch_layout, 0);
1220 av_opt_set_int(decoder->swr_ctx, "in_sample_rate", decoder->audio_codec_ctx->sample_rate, 0);
1221 av_opt_set_sample_fmt(decoder->swr_ctx, "in_sample_fmt", decoder->audio_codec_ctx->sample_fmt, 0);
1222
1223 AVChannelLayout out_ch_layout = AV_CHANNEL_LAYOUT_STEREO;
1224 av_opt_set_chlayout(decoder->swr_ctx, "out_chlayout", &out_ch_layout, 0);
1225 av_opt_set_int(decoder->swr_ctx, "out_sample_rate", TARGET_SAMPLE_RATE, 0);
1226 av_opt_set_sample_fmt(decoder->swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
1227 if (decoder->audio_codec_ctx->ch_layout.nb_channels == 1) {
1228 const double matrix[] = {1.0, 1.0};
1229 swr_set_matrix(decoder->swr_ctx, matrix, 1);
1230 }
1231
1232 if (swr_init(decoder->swr_ctx) < 0) {
1233 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to initialize swresample context");
1234 ffmpeg_decoder_destroy(decoder);
1235 return NULL;
1236 }
1237
1239 decoder->audio_buffer = SAFE_MALLOC(decoder->audio_buffer_size * sizeof(float), float *);
1240 if (!decoder->audio_buffer) {
1241 SET_ERRNO(ERROR_MEMORY, "Failed to allocate audio buffer");
1242 ffmpeg_decoder_destroy(decoder);
1243 return NULL;
1244 }
1245 }
1246
1247 // Initialize video frame prefetching system for stdin (same as file-based)
1248 if (decoder->video_stream_idx >= 0) {
1249 int width = decoder->video_codec_ctx->width;
1250 int height = decoder->video_codec_ctx->height;
1251
1252 // For stdin sources, dimensions might be invalid initially (0x0)
1253 // Use default dimensions for prefetch buffers; they'll be resized on first frame
1254 if (width <= 0 || height <= 0) {
1255 width = 1920; // Default width for stdin
1256 height = 1080; // Default height for stdin
1257 log_debug("stdin: Using default prefetch dimensions %dx%d (will be resized on first frame)", width, height);
1258 }
1259
1260 // Create two prefetch image buffers for double-buffering
1261 decoder->prefetch_image_a = image_new((size_t)width, (size_t)height);
1262 decoder->prefetch_image_b = image_new((size_t)width, (size_t)height);
1263 if (!decoder->prefetch_image_a || !decoder->prefetch_image_b) {
1264 SET_ERRNO(ERROR_MEMORY, "Failed to allocate prefetch image buffers");
1265 ffmpeg_decoder_destroy(decoder);
1266 return NULL;
1267 }
1268
1269 decoder->current_prefetch_image = decoder->prefetch_image_a;
1270 decoder->prefetch_frame_ready = false;
1271
1272 // Initialize prefetch mutex
1273 if (mutex_init(&decoder->prefetch_mutex, "ffmpeg_prefetch") != 0) {
1274 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch mutex");
1275 ffmpeg_decoder_destroy(decoder);
1276 return NULL;
1277 }
1278
1279 if (cond_init(&decoder->prefetch_cond, "ffmpeg_prefetch") != 0) {
1280 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch condition variable");
1281 mutex_destroy(&decoder->prefetch_mutex);
1282 ffmpeg_decoder_destroy(decoder);
1283 return NULL;
1284 }
1285
1286 if (mutex_init(&decoder->read_frame_mutex, "ffmpeg_read_frame") != 0) {
1287 SET_ERRNO(ERROR_MEMORY, "Failed to initialize read_frame mutex");
1288 mutex_destroy(&decoder->prefetch_mutex);
1289 cond_destroy(&decoder->prefetch_cond);
1290 ffmpeg_decoder_destroy(decoder);
1291 return NULL;
1292 }
1293 }
1294
1295 log_debug("FFmpeg decoder opened from stdin (video=%s, audio=%s)", decoder->video_stream_idx >= 0 ? "yes" : "no",
1296 decoder->audio_stream_idx >= 0 ? "yes" : "no");
1297
1298 NAMED_REGISTER_FFMPEG_DECODER(decoder, "stdin", NULL);
1299 NAMED_REGISTER_ATOMIC(&decoder->eof_reached, "ffmpeg_eof", decoder);
1300
1301 return decoder;
1302}
1303
1305 ASCII_OTEL_SCOPE(FFMPEG_DECODER_DESTROY);
1306 if (!decoder) {
1307 return;
1308 }
1309
1310 NAMED_UNREGISTER(&decoder->eof_reached);
1311 NAMED_UNREGISTER(decoder);
1312
1313 // Stop prefetch thread (signal it to stop and wait for it to finish)
1314 if (decoder->prefetch_thread_running) {
1315 mutex_lock(&decoder->prefetch_mutex);
1316 decoder->prefetch_should_stop = true;
1317 cond_signal(&decoder->prefetch_cond);
1318 mutex_unlock(&decoder->prefetch_mutex);
1319
1320 // Wait for thread to finish
1321 thread_join(&decoder->prefetch_thread, NULL);
1322 decoder->prefetch_thread_running = false;
1323 }
1324
1325 // Clean up prefetch state
1326 cond_destroy(&decoder->prefetch_cond);
1327 mutex_destroy(&decoder->prefetch_mutex);
1328 mutex_destroy(&decoder->read_frame_mutex);
1329
1330 // Free prefetch image buffers
1331 if (decoder->prefetch_image_a) {
1333 decoder->prefetch_image_a = NULL;
1334 }
1335 if (decoder->prefetch_image_b) {
1337 decoder->prefetch_image_b = NULL;
1338 }
1339
1340 // Don't destroy current_image - it points to one of the prefetch buffers
1341 // which have already been destroyed above
1342 decoder->current_image = NULL;
1343
1344 // Free audio buffer
1345 SAFE_FREE(decoder->audio_buffer);
1346
1347 // Free swscale context
1348 if (decoder->sws_ctx) {
1349 sws_freeContext(decoder->sws_ctx);
1350 decoder->sws_ctx = NULL;
1351 }
1352
1353 // Free swresample context
1354 if (decoder->swr_ctx) {
1355 swr_free(&decoder->swr_ctx);
1356 }
1357
1358 // Free frame and packet
1359 if (decoder->frame) {
1360 av_frame_free(&decoder->frame);
1361 }
1362 if (decoder->packet) {
1363 av_packet_free(&decoder->packet);
1364 }
1365
1366 // Free codec contexts
1367 if (decoder->video_codec_ctx) {
1368 avcodec_free_context(&decoder->video_codec_ctx);
1369 }
1370 if (decoder->audio_codec_ctx) {
1371 avcodec_free_context(&decoder->audio_codec_ctx);
1372 }
1373
1374 // Free format context
1375 if (decoder->format_ctx) {
1376 avformat_close_input(&decoder->format_ctx);
1377 }
1378
1379 // Free AVIO context (stdin only)
1380 if (decoder->avio_ctx) {
1381 // Free the opaque reader for stdin
1382 if (decoder->is_stdin) {
1383 SAFE_FREE(decoder->avio_ctx->opaque);
1384 }
1385 av_freep(&decoder->avio_ctx->buffer);
1386 avio_context_free(&decoder->avio_ctx);
1387 }
1388
1389 SAFE_FREE(decoder);
1390}
1391
1392/* ============================================================================
1393 * Video Operations
1394 * ============================================================================ */
1395
1397 asciichat_error_context_t *failure) {
1398 ASCII_OTEL_SCOPE(FFMPEG_DECODER_POLL_VIDEO);
1399 if (frame)
1400 *frame = NULL;
1401 if (failure)
1402 memset(failure, 0, sizeof(*failure));
1403 if (!decoder || !frame || !failure) {
1405 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid video poll outputs");
1406 if (failure && !errno_peek_since(scope, failure))
1407 failure->code = ERROR_INVALID_PARAM;
1408 return FFMPEG_FRAME_FAILED;
1409 }
1410 // Empty polls need no protected state. EOF and errors still confirm under
1411 // the mutex so a final pending frame takes precedence over termination.
1412 if (!atomic_load_explicit(&decoder->prefetch_frame_ready, memory_order_acquire) &&
1413 !atomic_load_explicit(&decoder->prefetch_failed, memory_order_acquire) && !ffmpeg_decoder_at_end(decoder))
1414 return FFMPEG_FRAME_WAITING;
1415
1416 mutex_lock(&decoder->prefetch_mutex);
1417 if (decoder->prefetch_frame_ready && decoder->current_prefetch_image) {
1418 // Taking the next image releases the previous reader-owned buffer.
1419 if (decoder->current_read_buffer == decoder->prefetch_image_a)
1420 decoder->buffer_a_in_use = false;
1421 else if (decoder->current_read_buffer == decoder->prefetch_image_b)
1422 decoder->buffer_b_in_use = false;
1423 *frame = decoder->current_prefetch_image;
1424 decoder->prefetch_frame_ready = false;
1425 if (*frame == decoder->prefetch_image_a)
1426 decoder->buffer_a_in_use = true;
1427 else if (*frame == decoder->prefetch_image_b)
1428 decoder->buffer_b_in_use = true;
1429 decoder->current_read_buffer = *frame;
1430 decoder->current_image = *frame;
1431 cond_signal(&decoder->prefetch_cond);
1432 mutex_unlock(&decoder->prefetch_mutex);
1433 return FFMPEG_FRAME_READY;
1434 }
1435 *failure = decoder->prefetch_failure;
1436 bool eof = ffmpeg_decoder_at_end(decoder);
1437 mutex_unlock(&decoder->prefetch_mutex);
1438 if (failure->code != ASCIICHAT_OK)
1439 return FFMPEG_FRAME_FAILED;
1441}
1442
1444 ASCII_OTEL_SCOPE(FFMPEG_DECODER_READ_VIDEO_FRAME);
1445 image_t *frame = NULL;
1446 asciichat_error_context_t failure = {0};
1447 if (ffmpeg_decoder_poll_video(decoder, &frame, &failure) == FFMPEG_FRAME_FAILED)
1448 errno_import(&failure);
1449 return frame;
1450}
1451
1460 if (!decoder || decoder->video_stream_idx < 0) {
1461 return SET_ERRNO(ERROR_INVALID_PARAM, "Decoder has no initialized video prefetch buffers");
1462 }
1463
1464 if (!decoder->prefetch_image_a || !decoder->prefetch_image_b) {
1465 return SET_ERRNO(ERROR_INVALID_PARAM, "Decoder has no initialized video prefetch buffers");
1466 }
1467
1468 // Already running
1469 if (decoder->prefetch_thread_running) {
1470 return ASCIICHAT_OK;
1471 }
1472
1473 // Reset stop flag and create thread
1474 decoder->prefetch_should_stop = false;
1475 memset(&decoder->prefetch_failure, 0, sizeof(decoder->prefetch_failure));
1476 atomic_store(&decoder->prefetch_failed, false);
1477
1478 int thread_err = platform_thread_create(&decoder->prefetch_thread, "ffmpeg_prefetch",
1479 ffmpeg_decoder_prefetch_thread_func, decoder);
1480 if (thread_err != 0) {
1481 return SET_ERRNO(ERROR_THREAD, "Failed to create video prefetch thread");
1482 }
1483
1484 decoder->prefetch_thread_running = true;
1485 return ASCIICHAT_OK;
1486}
1487
1492 if (!decoder || !decoder->prefetch_thread_running) {
1493 return;
1494 }
1495
1496 mutex_lock(&decoder->prefetch_mutex);
1497 decoder->prefetch_should_stop = true;
1498 cond_signal(&decoder->prefetch_cond);
1499 mutex_unlock(&decoder->prefetch_mutex);
1500 // Wait up to 2 seconds for thread to stop
1501 // The interrupt callback should cause av_read_frame to abort quickly
1502 if (thread_join_timeout(&decoder->prefetch_thread, NULL, 2000 * NS_PER_MS_INT) == ASCIICHAT_OK)
1503 decoder->prefetch_thread_running = false;
1504 // A timed-out worker remains owned and must be joined by decoder destruction.
1505 else
1506 log_warn("Video prefetch thread is still stopping");
1507}
1508
1509void ffmpeg_decoder_set_exit_callback(ffmpeg_decoder_t *decoder, bool (*should_exit_callback)(void *),
1510 void *user_data) {
1511 if (!decoder) {
1512 return;
1513 }
1514 atomic_ptr_store(&decoder->exit_callback_user_data, user_data);
1515 atomic_ptr_store(&decoder->should_exit_callback, (void *)should_exit_callback);
1516}
1517
1519 if (!decoder) {
1520 return false;
1521 }
1522 return decoder->prefetch_thread_running;
1523}
1524
1526 return decoder && decoder->video_stream_idx >= 0;
1527}
1528
1530 if (!decoder || decoder->video_stream_idx < 0) {
1531 return SET_ERRNO(ERROR_INVALID_PARAM, "Decoder has no video stream");
1532 }
1533
1534 if (width) {
1535 *width = decoder->video_codec_ctx->width;
1536 }
1537 if (height) {
1538 *height = decoder->video_codec_ctx->height;
1539 }
1540
1541 return ASCIICHAT_OK;
1542}
1543
1545 if (!decoder || decoder->video_stream_idx < 0) {
1546 return -1.0;
1547 }
1548
1549 AVStream *stream = decoder->format_ctx->streams[decoder->video_stream_idx];
1550
1551 // Try avg_frame_rate first (average frame rate from entire stream)
1552 double fps = av_q2d_safe(stream->avg_frame_rate);
1553
1554 // Fallback to r_frame_rate if avg_frame_rate is invalid or zero
1555 // r_frame_rate is the "real" frame rate based on codec parameters
1556 // This is more reliable for YouTube videos and some video codecs
1557 if (fps <= 0.0) {
1558 fps = av_q2d_safe(stream->r_frame_rate);
1559 }
1560
1561 return fps;
1562}
1563
1564/* ============================================================================
1565 * Audio Operations
1566 * ============================================================================ */
1567
1568size_t ffmpeg_decoder_read_audio_samples(ffmpeg_decoder_t *decoder, float *buffer, size_t num_samples) {
1569 ASCII_OTEL_SCOPE(FFMPEG_DECODER_READ_AUDIO_SAMPLES);
1570 // Keep channels and the playback clock aligned even with odd caller capacity.
1571 num_samples -= num_samples % TARGET_CHANNELS;
1572 if (!decoder || decoder->audio_stream_idx < 0 || !buffer || num_samples == 0) {
1573 return 0;
1574 }
1575
1576 mutex_lock(&decoder->read_frame_mutex);
1577 size_t samples_written = 0;
1578
1579 // First, drain any buffered samples
1580 if (decoder->audio_buffer_offset > 0) {
1581 size_t available = decoder->audio_buffer_offset;
1582 size_t to_copy = (available < num_samples) ? available : num_samples;
1583
1584 memcpy(buffer, decoder->audio_buffer, to_copy * sizeof(float));
1585 samples_written += to_copy;
1586
1587 // Shift buffer
1588 if (to_copy < available) {
1589 memmove(decoder->audio_buffer, decoder->audio_buffer + to_copy, (available - to_copy) * sizeof(float));
1590 }
1591 decoder->audio_buffer_offset -= to_copy;
1592
1593 if (samples_written >= num_samples) {
1594 decoder->audio_samples_read += samples_written / TARGET_CHANNELS;
1595 mutex_unlock(&decoder->read_frame_mutex);
1596 return samples_written;
1597 }
1598 }
1599
1600 // Read more packets to fill the request
1601 static uint64_t packet_count = 0;
1602
1603 while (samples_written < num_samples) {
1604 int ret = traced_packet_read(decoder->format_ctx, decoder->packet);
1605 if (ret < 0) {
1606 if (ret == AVERROR_EOF) {
1607 atomic_store_bool(&decoder->eof_reached, true);
1608 }
1609 break;
1610 }
1611
1612 // Check if this is an audio packet
1613 if (decoder->packet->stream_index != decoder->audio_stream_idx) {
1614 av_packet_unref(decoder->packet);
1615 continue;
1616 }
1617
1618 packet_count++;
1619 log_debug_every(US_PER_SEC_INT, "Audio packet #%lu: pts=%ld dts=%ld duration=%d size=%d", packet_count,
1620 decoder->packet->pts, decoder->packet->dts, decoder->packet->duration, decoder->packet->size);
1621
1622 // Send packet to decoder
1623 ret = traced_packet_send(decoder->audio_codec_ctx, decoder->packet);
1624 av_packet_unref(decoder->packet);
1625
1626 if (ret < 0) {
1627 char error_message[AV_ERROR_MAX_STRING_SIZE];
1628 av_strerror(ret, error_message, sizeof(error_message));
1629 log_warn_every(US_PER_SEC_INT, "Error sending audio packet to decoder: %s", error_message);
1630 continue;
1631 }
1632
1633 // Receive all decoded frames from this packet
1634 // Important: a single packet can produce multiple frames. Must drain all before next packet.
1635 while (1) {
1636 ret = traced_frame_receive(decoder->audio_codec_ctx, decoder->frame);
1637 if (ret == AVERROR(EAGAIN)) {
1638 break; // No more frames from this packet, get next packet
1639 } else if (ret < 0) {
1640 log_warn_every(US_PER_SEC_INT, "Error receiving audio frame from decoder");
1641 goto audio_read_done;
1642 }
1643
1644 // Update position tracking
1645 decoder->last_audio_pts =
1646 get_frame_pts_seconds(decoder->frame, decoder->format_ctx->streams[decoder->audio_stream_idx]->time_base);
1647
1648 // Resample the complete frame and retain output beyond this callback's
1649 // requested block. Flushing the resampler between packets would insert
1650 // padding into inputs whose sample rate differs from the output rate.
1651 uint8_t *out_ptr = (uint8_t *)decoder->audio_buffer;
1652 int converted =
1653 traced_audio_resample(decoder->swr_ctx, &out_ptr, (int)(decoder->audio_buffer_size / TARGET_CHANNELS),
1654 (const uint8_t **)decoder->frame->data, decoder->frame->nb_samples);
1655
1656 if (converted > 0) {
1657 size_t available = (size_t)converted * TARGET_CHANNELS;
1658 size_t remaining = num_samples - samples_written;
1659 size_t to_copy = available < remaining ? available : remaining;
1660 memcpy(buffer + samples_written, decoder->audio_buffer, to_copy * sizeof(float));
1661 samples_written += to_copy;
1662 decoder->audio_buffer_offset = available - to_copy;
1663 if (decoder->audio_buffer_offset > 0)
1664 memmove(decoder->audio_buffer, decoder->audio_buffer + to_copy, decoder->audio_buffer_offset * sizeof(float));
1665 }
1666
1667 if (samples_written >= num_samples) {
1668 goto audio_read_done;
1669 }
1670 }
1671 }
1672
1673audio_read_done:
1674 // Flush resampler buffer if we haven't filled the full request
1675 // The resampler may have buffered samples that need to be output
1676 if (samples_written < num_samples && atomic_load_bool(&decoder->eof_reached)) {
1677 int remaining_space = (int)((num_samples - samples_written) / TARGET_CHANNELS);
1678 uint8_t *out_ptr = (uint8_t *)(buffer + samples_written);
1679 int flushed = traced_audio_resample(decoder->swr_ctx, &out_ptr, remaining_space, NULL, 0);
1680 if (flushed > 0) {
1681 samples_written += (size_t)flushed * TARGET_CHANNELS;
1682 }
1683 }
1684
1685 // Update sample-based position tracking
1686 decoder->audio_samples_read += samples_written / TARGET_CHANNELS;
1687 mutex_unlock(&decoder->read_frame_mutex);
1688
1689 return samples_written;
1690}
1691
1693 return decoder && decoder->audio_stream_idx >= 0;
1694}
1695
1696/* ============================================================================
1697 * Playback Control
1698 * ============================================================================ */
1699
1703
1705 ASCII_OTEL_SCOPE(FFMPEG_DECODER_SEEK_TO_TIMESTAMP);
1706 if (!decoder) {
1707 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing decoder for seek");
1708 }
1709
1710 if (decoder->is_stdin) {
1711 return ERROR_NOT_SUPPORTED; // Cannot seek stdin
1712 }
1713
1714 // Hold mutex during entire seek operation to prevent race with prefetch thread
1715 mutex_lock(&decoder->prefetch_mutex);
1716
1717 // Set flag to pause prefetch thread via condition variable
1718 decoder->seeking_in_progress = true;
1719 ++decoder->seek_generation;
1720 // Serialize codec and partial-audio state with the existing packet reader.
1721 mutex_lock(&decoder->read_frame_mutex);
1722 // The old reader has stopped. Holding both guards prevents another read;
1723 // the seek must not cancel its own I/O through the interrupt callback.
1724 decoder->seeking_in_progress = false;
1725 if (decoder->format_ctx->pb && decoder->format_ctx->pb->error == AVERROR_EXIT)
1726 decoder->format_ctx->pb->error = 0;
1727
1728 // Convert seconds to FFmpeg time base units (AV_TIME_BASE = 1,000,000)
1729 int64_t target_ts = (int64_t)(timestamp_sec * AV_TIME_BASE);
1730
1731 // For HTTP streams, use simple keyframe seeking (faster than frame-accurate seeking)
1732 // HTTP seeking is expensive and can break stream state, so prefer speed over precision
1733 int seek_ret = av_seek_frame(decoder->format_ctx, -1, target_ts, AVSEEK_FLAG_BACKWARD);
1734 if (seek_ret < 0) {
1735 // Fallback: try without backward flag
1736 seek_ret = av_seek_frame(decoder->format_ctx, -1, target_ts, 0);
1737 }
1738
1739 if (seek_ret < 0) {
1740 decoder->seeking_in_progress = false;
1741 mutex_unlock(&decoder->read_frame_mutex);
1742 cond_signal(&decoder->prefetch_cond);
1743 mutex_unlock(&decoder->prefetch_mutex);
1744 return SET_ERRNO(ERROR_MEDIA_SEEK, "Failed to seek to timestamp %.2f seconds", timestamp_sec);
1745 }
1746
1747 // Flush codec buffers AFTER seeking
1748 if (decoder->video_codec_ctx) {
1749 avcodec_flush_buffers(decoder->video_codec_ctx);
1750 }
1751 if (decoder->audio_codec_ctx) {
1752 avcodec_flush_buffers(decoder->audio_codec_ctx);
1753 }
1754
1755 // Reset state
1756 atomic_store_bool(&decoder->eof_reached, false);
1757 decoder->video_draining = false;
1758 decoder->audio_buffer_offset = 0;
1759 // Clear any stale audio data in buffer
1760 if (decoder->audio_buffer) {
1761 memset(decoder->audio_buffer, 0, decoder->audio_buffer_size * sizeof(float));
1762 }
1763 decoder->last_video_pts = -1.0;
1764 decoder->last_audio_pts = -1.0;
1765 decoder->prefetch_frame_ready = false;
1766 // Set audio_samples_read to match the seek target so position tracking works correctly
1767 decoder->audio_samples_read = (uint64_t)(timestamp_sec * decoder->audio_sample_rate);
1768
1769 // The consumer retains its image across seeks until it takes a replacement.
1770
1771 // Resume prefetch thread
1772 decoder->seeking_in_progress = false;
1773 mutex_unlock(&decoder->read_frame_mutex);
1774 cond_signal(&decoder->prefetch_cond);
1775
1776 // Release mutex - prefetch thread can resume
1777 mutex_unlock(&decoder->prefetch_mutex);
1778
1779 return ASCIICHAT_OK;
1780}
1781
1783 return decoder && atomic_load_bool(&decoder->eof_reached);
1784}
1785
1787 if (!decoder || !decoder->format_ctx) {
1788 return -1.0;
1789 }
1790
1791 if (decoder->format_ctx->duration == AV_NOPTS_VALUE) {
1792 return -1.0;
1793 }
1794
1795 return (double)decoder->format_ctx->duration / AV_TIME_BASE;
1796}
1797
1799 if (!decoder) {
1800 return -1.0;
1801 }
1802
1803 // Prefer sample-based position tracking (continuous, works before frames are decoded)
1804 if (decoder->audio_sample_rate > 0 && decoder->audio_samples_read >= 0) {
1805 return (double)decoder->audio_samples_read / (double)decoder->audio_sample_rate;
1806 }
1807
1808 // Fallback to frame-based position if available
1809 if (decoder->last_video_pts >= 0.0) {
1810 return decoder->last_video_pts;
1811 } else if (decoder->last_audio_pts >= 0.0) {
1812 return decoder->last_audio_pts;
1813 }
1814
1815 return -1.0;
1816}
bool errno_peek_since(asciichat_errno_scope_t scope, asciichat_error_context_t *out)
void errno_scope_end(asciichat_errno_scope_t scope, asciichat_errno_outcome_t outcome)
bool errno_notifications_suppress(bool suppress)
asciichat_errno_scope_t errno_checkpoint(void)
asciichat_errno_scope_t errno_scope_begin(void)
asciichat_error_t errno_import(const asciichat_error_context_t *cause)
Synchronized per-thread pending error stacks and value snapshots. SET_ERRNO pushes,...
#define SET_ERRNO(code,...)
#define ASSERT_NO_ERRNO_SINCE(scope)
@ ASCIICHAT_ERRNO_HANDLED
@ ASCIICHAT_ERRNO_DISMISSED
#define LOG_ERRNO_IF_SET(message)
#define HAS_ERRNO_CODE_SINCE(scope, code)
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Named object registry for debugging — log identifiable resource names.
ffmpeg_decoder_t * ffmpeg_decoder_create_interruptible(const char *path, bool(*cancel)(void *), void *data)
#define MEDIA_FFMPEG_DECODER_DECODER_NAME_BUFFER_SIZE
#define TARGET_CHANNELS
#define AVIO_BUFFER_SIZE
ffmpeg_decoder_t * ffmpeg_decoder_create_with_request_options(const char *path, bool(*cancel)(void *), void *data, const char *headers, const char *cookies, const char *proxy)
#define TARGET_SAMPLE_RATE
🎞️ FFmpeg-based media decoder for video and audio streams
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t with automatic format specifier.
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
#define NAMED_REGISTER_FFMPEG_DECODER(decoder, name, parent_ptr)
Register an FFmpeg decoder with automatic format specifier.
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_MEDIA_SEEK
Definition error_codes.h:68
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_MEDIA_DECODE
Definition error_codes.h:67
@ ERROR_INVALID_PARAM
@ ERROR_MEDIA_OPEN
Definition error_codes.h:66
@ ERROR_THREAD
@ ERROR_NOT_SUPPORTED
Definition error_codes.h:69
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
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)
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.
ffmpeg_frame_status_t
Decode next video frame.
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_frame_status_t ffmpeg_decoder_poll_video(ffmpeg_decoder_t *decoder, image_t **frame, asciichat_error_context_t *failure)
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.
@ FFMPEG_FRAME_READY
@ FFMPEG_FRAME_WAITING
@ FFMPEG_FRAME_FAILED
@ FFMPEG_FRAME_EOF
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as a compact duration with configurable sub-minute precision.
Definition time.c:374
#define TIME_DURATION_BUFFER_SIZE
Definition time.h:40
#define NS_PER_MS_INT
Definition time.h:168
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition time.c:123
#define US_PER_SEC_INT
Definition time.h:173
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
#define EAGAIN
bool url_is_valid(const char *url)
URL validation using shared parsing and ICU hostname checks.
Definition url.c:159
void image_destroy(image_t *image)
Destroy an image allocated with image_new().
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
Capture stdout/stderr and redirect to logging system.
#define LOG_IO(prefix, block)
Definition io.h:104
📝 Logging API with multiple log levels and terminal output control
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:749
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:755
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:746
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
#define ASCII_OTEL_ATTR_INT(key, value)
Definition otel.h:67
#define ASCII_OTEL_ATTR_STRING(key, value)
Definition otel.h:66
🔤 String Manipulation and Shell Escaping Utilities
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...
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.
_Atomic bool seeking_in_progress
Signal to pause prefetch thread during seek.
platform_thread_t prefetch_thread
Prefetch thread handle.
atomic_t eof_reached
Whether end of file was reached.
atomic_ptr_t exit_callback_user_data
User data for exit callback.
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.
atomic_ptr_t should_exit_callback
Callback to check if app should exit.
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 traced_packet_read() calls from video and audio threads.
struct SwrContext * swr_ctx
Software resampler for format conversion.
AVPacket * packet
Reusable packet for reading.
int video_stream_idx
Video stream index (-1 if none).
_Atomic bool prefetch_should_stop
Signal to stop prefetch thread.
unsigned char * avio_buffer
Buffer for custom I/O.
_Atomic bool prefetch_failed
double last_audio_pts
Last audio presentation timestamp.
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.
_Atomic bool prefetch_frame_ready
Whether current_prefetch_image has valid data.
asciichat_error_context_t prefetch_failure
_Atomic uint64_t seek_generation
Detect seeks before and after packet reads.
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.
SIMD-friendly storage for one RGB pixel. r, g, and b hold the color channels and padding keeps the la...
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).
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.
⏱️ High-precision timing utilities using sokol_time.h and uthash
🌐 Production-Grade URL Validation and Parsing