ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
ffmpeg_decoder.c
Go to the documentation of this file.
1
6#include <ascii-chat/media/ffmpeg_decoder.h>
7#include <ascii-chat/common.h>
8#include <ascii-chat/log/logging.h>
9#include <ascii-chat/asciichat_errno.h>
10#include <ascii-chat/video/image.h>
11#include <ascii-chat/platform/system.h>
12#include <ascii-chat/platform/thread.h>
13#include <ascii-chat/util/time.h>
14#include <ascii-chat/util/url.h>
15#include <ascii-chat/options/options.h>
16
17#include <libavformat/avformat.h>
18#include <libavcodec/avcodec.h>
19#include <libavutil/imgutils.h>
20#include <libavutil/opt.h>
21#include <libavutil/log.h>
22#include <libswscale/swscale.h>
23#include <libswresample/swresample.h>
24#include <string.h>
25#include <inttypes.h>
26
27/* ============================================================================
28 * FFmpeg Logging Suppression
29 * ============================================================================ */
30
35static void ffmpeg_silent_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
36 (void)avcl; // unused
37 (void)level; // unused
38 (void)fmt; // unused
39 (void)vl; // unused
40 // Do nothing - silently discard all FFmpeg logs
41}
42
43/* ============================================================================
44 * Constants
45 * ============================================================================ */
46
48#define TARGET_SAMPLE_RATE 48000
49
51#define TARGET_CHANNELS 1
52
54#define AVIO_BUFFER_SIZE (64 * 1024)
55
56/* ============================================================================
57 * FFmpeg Decoder Structure
58 * ============================================================================ */
59
67 // Format context
68 AVFormatContext *format_ctx;
69
70 // Video stream
71 AVCodecContext *video_codec_ctx;
73 struct SwsContext *sws_ctx;
74
75 // Audio stream
76 AVCodecContext *audio_codec_ctx;
78 struct SwrContext *swr_ctx;
79
80 // Frame and packet buffers
81 AVFrame *frame;
82 AVPacket *packet;
83
84 // Decoded image cache - double-buffered for prefetching
85 image_t *current_image;
86
87 // Audio sample buffer (for partial frame handling)
88 float *audio_buffer;
91
92 // Background thread for frame prefetching (reduces YouTube HTTP blocking)
93 asciichat_thread_t prefetch_thread;
103
104 // Track which buffer is being read by main thread
108
109 // State flags
111 bool is_stdin;
112
113 // Stdin I/O context
114 AVIOContext *avio_ctx;
115 unsigned char *avio_buffer;
116
117 // Position tracking
120
121 // Sample-based position tracking
124};
125
126/* ============================================================================
127 * Stdin I/O Callbacks
128 * ============================================================================ */
129
133static int stdin_read_packet(void *opaque, uint8_t *buf, int buf_size) {
134 (void)opaque; // Unused
135
136 size_t bytes_read = fread(buf, 1, (size_t)buf_size, stdin);
137 if (bytes_read == 0) {
138 if (feof(stdin)) {
139 return AVERROR_EOF;
140 }
141 return AVERROR(EIO);
142 }
143
144 return (int)bytes_read;
145}
146
147/* ============================================================================
148 * Helper Functions
149 * ============================================================================ */
150
154static inline double av_q2d_safe(AVRational r) {
155 return (r.den != 0) ? ((double)r.num / (double)r.den) : 0.0;
156}
157
161static double get_frame_pts_seconds(AVFrame *frame, AVRational time_base) {
162 if (frame->pts == AV_NOPTS_VALUE) {
163 return -1.0;
164 }
165 return (double)frame->pts * av_q2d_safe(time_base);
166}
167
174static int ffmpeg_interrupt_callback(void *opaque) {
175 ffmpeg_decoder_t *decoder = (ffmpeg_decoder_t *)opaque;
176 if (!decoder) {
177 return 0;
178 }
179 // Interrupt av_read_frame() if a seek is in progress
180 return decoder->seeking_in_progress ? 1 : 0;
181}
182
190static void *ffmpeg_decoder_prefetch_thread_func(void *arg) {
191 ffmpeg_decoder_t *decoder = (ffmpeg_decoder_t *)arg;
192 if (!decoder || !decoder->prefetch_image_a || !decoder->prefetch_image_b) {
193 return NULL;
194 }
195
196 log_debug("Video prefetch thread started");
197 bool use_image_a = true; // Track which buffer we're using (critical for correct buffer swapping)
198
199 while (true) {
200 mutex_lock(&decoder->prefetch_mutex);
201
202 // Check if thread should stop
203 bool should_stop = decoder->prefetch_should_stop || decoder->eof_reached;
204 if (should_stop) {
205 mutex_unlock(&decoder->prefetch_mutex);
206 break;
207 }
208
209 // Pause if seek is in progress - wait for signal to continue
210 // cond_wait automatically releases and re-acquires mutex
211 while (decoder->seeking_in_progress) {
212 cond_wait(&decoder->prefetch_cond, &decoder->prefetch_mutex);
213 }
214
215 // Mutex is re-acquired after cond_wait - KEEP IT HELD
216
217 // Check if the buffer we want to use is still in use by the main thread
218 // If so, skip this iteration and try again later
219 bool buffer_in_use = use_image_a ? decoder->buffer_a_in_use : decoder->buffer_b_in_use;
220 if (buffer_in_use) {
221 // Can't use this buffer yet - main thread is still rendering it
222 mutex_unlock(&decoder->prefetch_mutex);
223 platform_sleep_us(1 * US_PER_MS_INT); // 1ms - brief sleep before retry
224 continue;
225 }
226
227 uint64_t read_start_ns = time_get_ns();
228 bool frame_decoded = false;
229
230 // Release mutex before blocking av_read_frame() call
231 // The seeking_in_progress flag prevents av_seek_frame() races
232 mutex_unlock(&decoder->prefetch_mutex);
233
234 // Read packets until we get a video frame - MUTEX RELEASED
235 while (true) {
236 int ret = av_read_frame(decoder->format_ctx, decoder->packet);
237 if (ret < 0) {
238 if (ret == AVERROR_EOF) {
239 decoder->eof_reached = true;
240 }
241 break;
242 }
243
244 // Check if this is a video packet
245 if (decoder->packet->stream_index != decoder->video_stream_idx) {
246 av_packet_unref(decoder->packet);
247 continue;
248 }
249
250 // Send packet to decoder
251 ret = avcodec_send_packet(decoder->video_codec_ctx, decoder->packet);
252 av_packet_unref(decoder->packet);
253
254 if (ret < 0) {
255 continue;
256 }
257
258 // Receive decoded frame
259 ret = avcodec_receive_frame(decoder->video_codec_ctx, decoder->frame);
260 if (ret == AVERROR(EAGAIN)) {
261 continue; // Need more packets
262 } else if (ret < 0) {
263 break;
264 }
265
266 // Frame decoded - mutex still held
267 // Update position tracking
268 decoder->last_video_pts =
269 get_frame_pts_seconds(decoder->frame, decoder->format_ctx->streams[decoder->video_stream_idx]->time_base);
270
271 // Convert frame to RGB24
272 int width = decoder->video_codec_ctx->width;
273 int height = decoder->video_codec_ctx->height;
274
275 // Get current decode buffer based on which one we're using
276 // (Buffer availability was already checked before entering the decode loop)
277 image_t *decode_buffer = use_image_a ? decoder->prefetch_image_a : decoder->prefetch_image_b;
278
279 // Reallocate if needed (width/height changed)
280 if (decode_buffer->w != width || decode_buffer->h != height) {
281 image_destroy(decode_buffer);
282 decode_buffer = image_new((size_t)width, (size_t)height);
283 if (!decode_buffer) {
284 log_error("Failed to allocate prefetch image buffer");
285 break;
286 }
287 // Update decoder's pointer to the reallocated buffer
288 if (use_image_a) {
289 decoder->prefetch_image_a = decode_buffer;
290 } else {
291 decoder->prefetch_image_b = decode_buffer;
292 }
293 }
294
295 // Convert pixel format (rgb_pixel_t is 3 bytes: r, g, b)
296 uint8_t *dst_data[1] = {(uint8_t *)decode_buffer->pixels};
297 int dst_linesize[1] = {width * 3};
298
299 // Lazy initialize swscale context if not done at startup (happens with HTTP/stdin streams)
300 if (!decoder->sws_ctx) {
301 if (width > 0 && height > 0 && decoder->video_codec_ctx->pix_fmt != AV_PIX_FMT_NONE) {
302 decoder->sws_ctx = sws_getContext(width, height, decoder->video_codec_ctx->pix_fmt, width, height,
303 AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
304 if (!decoder->sws_ctx) {
305 log_error("Failed to create swscale context on first frame");
306 break;
307 }
308 log_debug("Lazy initialized swscale context with %dx%d", width, height);
309 } else {
310 log_error("Cannot initialize swscale: invalid dimensions or pixel format");
311 break;
312 }
313 }
314
315 sws_scale(decoder->sws_ctx, (const uint8_t *const *)decoder->frame->data, decoder->frame->linesize, 0, height,
316 dst_data, dst_linesize);
317
318 frame_decoded = true;
319 break; // Exit while loop
320 }
321
322 // Re-acquire mutex to update prefetch state
323 mutex_lock(&decoder->prefetch_mutex);
324
325 // Mutex now held - update prefetch state
326 if (frame_decoded) {
327 // Update shared prefetch state while mutex is held
328 }
329
330 mutex_unlock(&decoder->prefetch_mutex); // Release at end of main loop iteration
331
332 if (frame_decoded) {
333 uint64_t read_time_ns = time_elapsed_ns(read_start_ns, time_get_ns());
334 double read_ms = (double)read_time_ns / NS_PER_MS;
335
336 // Get current decode buffer
337 image_t *decode_buffer = use_image_a ? decoder->prefetch_image_a : decoder->prefetch_image_b;
338
339 // Update the current prefetch image (main thread will pull from this)
340 mutex_lock(&decoder->prefetch_mutex);
341 decoder->current_prefetch_image = decode_buffer;
342 decoder->prefetch_frame_ready = true;
343 mutex_unlock(&decoder->prefetch_mutex);
344
345 log_dev_every(5 * US_PER_SEC_INT, "PREFETCH: decoded frame in %.2f ms", read_ms);
346
347 // Switch to the other buffer for next iteration (MUST use boolean flag, not pointer comparison)
348 use_image_a = !use_image_a;
349 } else {
350 // EOF or error - exit thread
351 break;
352 }
353 }
354
355 log_debug("Video prefetch thread stopped");
356 return NULL;
357}
358
362static asciichat_error_t open_codec_context(AVFormatContext *fmt_ctx, enum AVMediaType type, int *stream_idx,
363 AVCodecContext **codec_ctx) {
364 int ret = av_find_best_stream(fmt_ctx, type, -1, -1, NULL, 0);
365 if (ret < 0) {
366 // Stream not found - not an error, just means no stream of this type
367 *stream_idx = -1;
368 *codec_ctx = NULL;
369 return ASCIICHAT_OK;
370 }
371
372 *stream_idx = ret;
373 AVStream *stream = fmt_ctx->streams[ret];
374
375 // Find decoder
376 const AVCodec *codec = avcodec_find_decoder(stream->codecpar->codec_id);
377 if (!codec) {
378 return SET_ERRNO(ERROR_MEDIA_DECODE, "Codec not found for stream %d", ret);
379 }
380
381 // Allocate codec context
382 *codec_ctx = avcodec_alloc_context3(codec);
383 if (!*codec_ctx) {
384 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate codec context");
385 }
386
387 // Copy codec parameters
388 if (avcodec_parameters_to_context(*codec_ctx, stream->codecpar) < 0) {
389 avcodec_free_context(codec_ctx);
390 return SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to copy codec parameters");
391 }
392
393 // Open codec
394 if (avcodec_open2(*codec_ctx, codec, NULL) < 0) {
395 avcodec_free_context(codec_ctx);
396 return SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to open codec");
397 }
398
399 return ASCIICHAT_OK;
400}
401
402/* ============================================================================
403 * Decoder Lifecycle
404 * ============================================================================ */
405
407 if (!path) {
408 SET_ERRNO(ERROR_INVALID_PARAM, "Path is NULL");
409 return NULL;
410 }
411
412 // Suppress FFmpeg's verbose debug logging (H.264 codec warnings, etc.)
413 // Only set this once, it's a global setting
414 static bool ffmpeg_log_level_set = false;
415 if (!ffmpeg_log_level_set) {
416 av_log_set_level(AV_LOG_QUIET); // Suppress all FFmpeg logging
417 av_log_set_callback(ffmpeg_silent_log_callback); // Install silent callback to discard all output
418 ffmpeg_log_level_set = true;
419 }
420
421 ffmpeg_decoder_t *decoder = SAFE_MALLOC(sizeof(ffmpeg_decoder_t), ffmpeg_decoder_t *);
422 if (!decoder) {
423 SET_ERRNO(ERROR_MEMORY, "Failed to allocate decoder");
424 return NULL;
425 }
426
427 memset(decoder, 0, sizeof(*decoder));
428 decoder->video_stream_idx = -1;
429 decoder->audio_stream_idx = -1;
430 decoder->last_video_pts = -1.0;
431 decoder->last_audio_pts = -1.0;
432
433 // Suppress FFmpeg's probing output by redirecting both stdout and stderr to /dev/null
434 // FFmpeg may write directly to either stream, so we suppress both
435 platform_stderr_redirect_handle_t stdio_handle = platform_stdout_stderr_redirect_to_null();
436
437 // Configure FFmpeg options for HTTP streaming performance
438 AVDictionary *options = NULL;
439
440 // For HTTP/HTTPS streams: enable fast probing and reconnection (validated via production-grade URL regex)
441 if (path && url_is_valid(path)) {
442 // Limit probing to 32KB for faster format detection
443 av_dict_set(&options, "probesize", "32768", 0);
444 // Analyze for 100ms max to determine streams quickly
445 av_dict_set(&options, "analyzeduration", "100000", 0);
446 // Enable auto-reconnection for interrupted connections
447 av_dict_set(&options, "reconnect", "1", 0);
448 // Allow reconnection for streamed protocols
449 av_dict_set(&options, "reconnect_streamed", "1", 0);
450 // Set reasonable I/O timeout (10 seconds)
451 av_dict_set(&options, "rw_timeout", "10000000", 0);
452 // Enable HTTP persistent connection (keep-alive) for better performance
453 av_dict_set(&options, "http_persistent", "1", 0);
454 // Reduce connect timeout to fail faster if server is unreachable
455 av_dict_set(&options, "connect_timeout", "5000000", 0);
456 }
457
458 // Open input file
459 int ret = avformat_open_input(&decoder->format_ctx, path, NULL, &options);
460 av_dict_free(&options); // Free options dictionary
461
462 if (ret < 0) {
463 platform_stdout_stderr_restore(stdio_handle);
464 SET_ERRNO(ERROR_MEDIA_OPEN, "Failed to open media file: %s", path);
465 SAFE_FREE(decoder);
466 return NULL;
467 }
468
469 // Find stream info
470 if (avformat_find_stream_info(decoder->format_ctx, NULL) < 0) {
471 platform_stdout_stderr_restore(stdio_handle);
472 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to find stream info");
473 avformat_close_input(&decoder->format_ctx);
474 SAFE_FREE(decoder);
475 return NULL;
476 }
477
478 // Install interrupt callback to allow seeking to interrupt long av_read_frame() calls
479 // Do this AFTER finding stream info to ensure format context is fully initialized
480 if (decoder->format_ctx) {
481 decoder->format_ctx->interrupt_callback.callback = ffmpeg_interrupt_callback;
482 decoder->format_ctx->interrupt_callback.opaque = decoder;
483 }
484
485 // Restore stdout and stderr after FFmpeg initialization
486 platform_stdout_stderr_restore(stdio_handle);
487
488 // Open video codec
489 asciichat_error_t err = open_codec_context(decoder->format_ctx, AVMEDIA_TYPE_VIDEO, &decoder->video_stream_idx,
490 &decoder->video_codec_ctx);
491 if (err != ASCIICHAT_OK) {
492 log_warn("Failed to open video codec (file may be audio-only)");
493 }
494
495 // Open audio codec - audio is enabled by default (no option needed)
496 // Always try to open audio codec, don't rely on GET_OPTION(audio_enabled) which has a default issue
497 err = open_codec_context(decoder->format_ctx, AVMEDIA_TYPE_AUDIO, &decoder->audio_stream_idx,
498 &decoder->audio_codec_ctx);
499 if (err != ASCIICHAT_OK) {
500 log_debug("No audio codec found (file may be video-only or audio codec not available)");
501 decoder->audio_stream_idx = -1;
502 decoder->audio_codec_ctx = NULL;
503 }
504
505 // Require at least one stream
506 if (decoder->video_stream_idx < 0 && decoder->audio_stream_idx < 0) {
507 SET_ERRNO(ERROR_MEDIA_DECODE, "No video or audio streams found");
508 ffmpeg_decoder_destroy(decoder);
509 return NULL;
510 }
511
512 // Allocate frame and packet
513 decoder->frame = av_frame_alloc();
514 decoder->packet = av_packet_alloc();
515 if (!decoder->frame || !decoder->packet) {
516 SET_ERRNO(ERROR_MEMORY, "Failed to allocate frame/packet");
517 ffmpeg_decoder_destroy(decoder);
518 return NULL;
519 }
520
521 // Initialize swscale context for video if present
522 if (decoder->video_codec_ctx) {
523 // Validate codec context has valid dimensions and pixel format
524 // For HTTP streams, these might not be valid until first frame is read
525 if (decoder->video_codec_ctx->width <= 0 || decoder->video_codec_ctx->height <= 0) {
526 log_warn("Video codec has invalid dimensions (%dx%d), will initialize swscale on first frame",
527 decoder->video_codec_ctx->width, decoder->video_codec_ctx->height);
528 // Don't create swscale context yet - will create it lazily on first frame read
529 } else if (decoder->video_codec_ctx->pix_fmt == AV_PIX_FMT_NONE) {
530 log_warn("Video codec has invalid pixel format, will initialize swscale on first frame");
531 // Don't create swscale context yet - will create it lazily on first frame read
532 } else {
533 // Create swscale context with valid parameters
534 decoder->sws_ctx =
535 sws_getContext(decoder->video_codec_ctx->width, decoder->video_codec_ctx->height,
536 decoder->video_codec_ctx->pix_fmt, decoder->video_codec_ctx->width,
537 decoder->video_codec_ctx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
538 if (!decoder->sws_ctx) {
539 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to create swscale context");
540 ffmpeg_decoder_destroy(decoder);
541 return NULL;
542 }
543 }
544 }
545
546 // Initialize swresample context for audio if present
547 if (decoder->audio_codec_ctx) {
548 // Store output sample rate for position tracking
550
551 // Allocate resampler context
552 decoder->swr_ctx = swr_alloc();
553 if (!decoder->swr_ctx) {
554 SET_ERRNO(ERROR_MEMORY, "Failed to allocate swresample context");
555 ffmpeg_decoder_destroy(decoder);
556 return NULL;
557 }
558
559 // Set options
560 av_opt_set_chlayout(decoder->swr_ctx, "in_chlayout", &decoder->audio_codec_ctx->ch_layout, 0);
561 av_opt_set_int(decoder->swr_ctx, "in_sample_rate", decoder->audio_codec_ctx->sample_rate, 0);
562 av_opt_set_sample_fmt(decoder->swr_ctx, "in_sample_fmt", decoder->audio_codec_ctx->sample_fmt, 0);
563
564 AVChannelLayout out_ch_layout = AV_CHANNEL_LAYOUT_MONO;
565 av_opt_set_chlayout(decoder->swr_ctx, "out_chlayout", &out_ch_layout, 0);
566 av_opt_set_int(decoder->swr_ctx, "out_sample_rate", TARGET_SAMPLE_RATE, 0);
567 av_opt_set_sample_fmt(decoder->swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
568
569 // Initialize
570 if (swr_init(decoder->swr_ctx) < 0) {
571 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to initialize swresample context");
572 ffmpeg_decoder_destroy(decoder);
573 return NULL;
574 }
575
576 // Allocate audio buffer (10 seconds worth)
578 decoder->audio_buffer = SAFE_MALLOC(decoder->audio_buffer_size * sizeof(float), float *);
579 if (!decoder->audio_buffer) {
580 SET_ERRNO(ERROR_MEMORY, "Failed to allocate audio buffer");
581 ffmpeg_decoder_destroy(decoder);
582 return NULL;
583 }
584 }
585
586 // Initialize video frame prefetching system (for YouTube streaming)
587 if (decoder->video_stream_idx >= 0) {
588 int width = decoder->video_codec_ctx->width;
589 int height = decoder->video_codec_ctx->height;
590
591 // Create two prefetch image buffers for double-buffering
592 decoder->prefetch_image_a = image_new((size_t)width, (size_t)height);
593 decoder->prefetch_image_b = image_new((size_t)width, (size_t)height);
594 if (!decoder->prefetch_image_a || !decoder->prefetch_image_b) {
595 SET_ERRNO(ERROR_MEMORY, "Failed to allocate prefetch image buffers");
596 ffmpeg_decoder_destroy(decoder);
597 return NULL;
598 }
599
600 decoder->current_prefetch_image = decoder->prefetch_image_a;
601 decoder->prefetch_frame_ready = false;
602
603 // Initialize prefetch mutex
604 if (mutex_init(&decoder->prefetch_mutex) != 0) {
605 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch mutex");
606 ffmpeg_decoder_destroy(decoder);
607 return NULL;
608 }
609
610 if (cond_init(&decoder->prefetch_cond) != 0) {
611 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch condition variable");
612 mutex_destroy(&decoder->prefetch_mutex);
613 ffmpeg_decoder_destroy(decoder);
614 return NULL;
615 }
616
617 decoder->prefetch_thread_running = false;
618 decoder->prefetch_should_stop = false;
619 }
620
621 log_debug("FFmpeg decoder opened: %s (video=%s, audio=%s)", path, decoder->video_stream_idx >= 0 ? "yes" : "no",
622 decoder->audio_stream_idx >= 0 ? "yes" : "no");
623
624 return decoder;
625}
626
628 ffmpeg_decoder_t *decoder = SAFE_MALLOC(sizeof(ffmpeg_decoder_t), ffmpeg_decoder_t *);
629 if (!decoder) {
630 SET_ERRNO(ERROR_MEMORY, "Failed to allocate decoder");
631 return NULL;
632 }
633
634 memset(decoder, 0, sizeof(*decoder));
635 decoder->video_stream_idx = -1;
636 decoder->audio_stream_idx = -1;
637 decoder->is_stdin = true;
638 decoder->last_video_pts = -1.0;
639 decoder->last_audio_pts = -1.0;
640
641 // Allocate AVIO buffer
642 decoder->avio_buffer = SAFE_MALLOC(AVIO_BUFFER_SIZE, unsigned char *);
643 if (!decoder->avio_buffer) {
644 SET_ERRNO(ERROR_MEMORY, "Failed to allocate AVIO buffer");
645 SAFE_FREE(decoder);
646 return NULL;
647 }
648
649 // Create AVIO context for stdin
650 decoder->avio_ctx = avio_alloc_context(decoder->avio_buffer, AVIO_BUFFER_SIZE,
651 0, // write_flag
652 NULL, // opaque
653 stdin_read_packet,
654 NULL, // write_packet
655 NULL // seek (stdin is not seekable)
656 );
657
658 if (!decoder->avio_ctx) {
659 SET_ERRNO(ERROR_MEMORY, "Failed to create AVIO context");
660 SAFE_FREE(decoder->avio_buffer);
661 SAFE_FREE(decoder);
662 return NULL;
663 }
664
665 // Allocate format context
666 decoder->format_ctx = avformat_alloc_context();
667 if (!decoder->format_ctx) {
668 SET_ERRNO(ERROR_MEMORY, "Failed to allocate format context");
669 av_freep(&decoder->avio_ctx->buffer);
670 avio_context_free(&decoder->avio_ctx);
671 SAFE_FREE(decoder);
672 return NULL;
673 }
674
675 decoder->format_ctx->pb = decoder->avio_ctx;
676
677 // Suppress FFmpeg's probing output by redirecting both stdout and stderr to /dev/null
678 platform_stderr_redirect_handle_t stdio_handle = platform_stdout_stderr_redirect_to_null();
679
680 // Open input from stdin
681 if (avformat_open_input(&decoder->format_ctx, NULL, NULL, NULL) < 0) {
682 platform_stdout_stderr_restore(stdio_handle);
683 SET_ERRNO(ERROR_MEDIA_OPEN, "Failed to open stdin");
684 av_freep(&decoder->avio_ctx->buffer);
685 avio_context_free(&decoder->avio_ctx);
686 avformat_free_context(decoder->format_ctx);
687 SAFE_FREE(decoder);
688 return NULL;
689 }
690
691 // Find stream info
692 if (avformat_find_stream_info(decoder->format_ctx, NULL) < 0) {
693 platform_stdout_stderr_restore(stdio_handle);
694 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to find stream info from stdin");
695 ffmpeg_decoder_destroy(decoder);
696 return NULL;
697 }
698
699 platform_stdout_stderr_restore(stdio_handle);
700
701 // Open codecs (same as file-based decoder)
702 asciichat_error_t err = open_codec_context(decoder->format_ctx, AVMEDIA_TYPE_VIDEO, &decoder->video_stream_idx,
703 &decoder->video_codec_ctx);
704 if (err != ASCIICHAT_OK) {
705 log_warn("Failed to open video codec from stdin");
706 }
707
708 if (GET_OPTION(audio_enabled)) {
709 err = open_codec_context(decoder->format_ctx, AVMEDIA_TYPE_AUDIO, &decoder->audio_stream_idx,
710 &decoder->audio_codec_ctx);
711 if (err != ASCIICHAT_OK) {
712 log_warn("Failed to open audio codec from stdin");
713 }
714 } else {
715 decoder->audio_stream_idx = -1;
716 decoder->audio_codec_ctx = NULL;
717 log_debug("Audio decoding disabled by user option");
718 }
719
720 if (decoder->video_stream_idx < 0 && decoder->audio_stream_idx < 0) {
721 SET_ERRNO(ERROR_MEDIA_DECODE, "No video or audio streams found in stdin");
722 ffmpeg_decoder_destroy(decoder);
723 return NULL;
724 }
725
726 // Allocate frame and packet
727 decoder->frame = av_frame_alloc();
728 decoder->packet = av_packet_alloc();
729 if (!decoder->frame || !decoder->packet) {
730 SET_ERRNO(ERROR_MEMORY, "Failed to allocate frame/packet");
731 ffmpeg_decoder_destroy(decoder);
732 return NULL;
733 }
734
735 // Initialize swscale/swresample (same as file-based)
736 if (decoder->video_codec_ctx) {
737 // Validate codec context has valid dimensions and pixel format
738 // For stdin/HTTP streams, these might not be valid until first frame is read
739 if (decoder->video_codec_ctx->width <= 0 || decoder->video_codec_ctx->height <= 0) {
740 log_warn("Video codec has invalid dimensions (%dx%d), will initialize swscale on first frame",
741 decoder->video_codec_ctx->width, decoder->video_codec_ctx->height);
742 // Don't create swscale context yet - will create it lazily on first frame read
743 } else if (decoder->video_codec_ctx->pix_fmt == AV_PIX_FMT_NONE) {
744 log_warn("Video codec has invalid pixel format, will initialize swscale on first frame");
745 // Don't create swscale context yet - will create it lazily on first frame read
746 } else {
747 // Create swscale context with valid parameters
748 decoder->sws_ctx =
749 sws_getContext(decoder->video_codec_ctx->width, decoder->video_codec_ctx->height,
750 decoder->video_codec_ctx->pix_fmt, decoder->video_codec_ctx->width,
751 decoder->video_codec_ctx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
752 if (!decoder->sws_ctx) {
753 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to create swscale context");
754 ffmpeg_decoder_destroy(decoder);
755 return NULL;
756 }
757 }
758 }
759
760 if (decoder->audio_codec_ctx) {
761 decoder->swr_ctx = swr_alloc();
762 if (!decoder->swr_ctx) {
763 SET_ERRNO(ERROR_MEMORY, "Failed to allocate swresample context");
764 ffmpeg_decoder_destroy(decoder);
765 return NULL;
766 }
767
768 av_opt_set_chlayout(decoder->swr_ctx, "in_chlayout", &decoder->audio_codec_ctx->ch_layout, 0);
769 av_opt_set_int(decoder->swr_ctx, "in_sample_rate", decoder->audio_codec_ctx->sample_rate, 0);
770 av_opt_set_sample_fmt(decoder->swr_ctx, "in_sample_fmt", decoder->audio_codec_ctx->sample_fmt, 0);
771
772 AVChannelLayout out_ch_layout = AV_CHANNEL_LAYOUT_MONO;
773 av_opt_set_chlayout(decoder->swr_ctx, "out_chlayout", &out_ch_layout, 0);
774 av_opt_set_int(decoder->swr_ctx, "out_sample_rate", TARGET_SAMPLE_RATE, 0);
775 av_opt_set_sample_fmt(decoder->swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
776
777 if (swr_init(decoder->swr_ctx) < 0) {
778 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to initialize swresample context");
779 ffmpeg_decoder_destroy(decoder);
780 return NULL;
781 }
782
784 decoder->audio_buffer = SAFE_MALLOC(decoder->audio_buffer_size * sizeof(float), float *);
785 if (!decoder->audio_buffer) {
786 SET_ERRNO(ERROR_MEMORY, "Failed to allocate audio buffer");
787 ffmpeg_decoder_destroy(decoder);
788 return NULL;
789 }
790 }
791
792 log_debug("FFmpeg decoder opened from stdin (video=%s, audio=%s)", decoder->video_stream_idx >= 0 ? "yes" : "no",
793 decoder->audio_stream_idx >= 0 ? "yes" : "no");
794
795 return decoder;
796}
797
799 if (!decoder) {
800 return;
801 }
802
803 // Stop prefetch thread (signal it to stop and wait for it to finish)
804 if (decoder->prefetch_thread_running) {
805 mutex_lock(&decoder->prefetch_mutex);
806 decoder->prefetch_should_stop = true;
807 mutex_unlock(&decoder->prefetch_mutex);
808
809 // Wait for thread to finish
810 asciichat_thread_join(&decoder->prefetch_thread, NULL);
811 decoder->prefetch_thread_running = false;
812 }
813
814 // Clean up prefetch state
815 cond_destroy(&decoder->prefetch_cond);
816 mutex_destroy(&decoder->prefetch_mutex);
817
818 // Free prefetch image buffers
819 if (decoder->prefetch_image_a) {
821 decoder->prefetch_image_a = NULL;
822 }
823 if (decoder->prefetch_image_b) {
825 decoder->prefetch_image_b = NULL;
826 }
827
828 // Don't destroy current_image - it points to one of the prefetch buffers
829 // which have already been destroyed above
830 decoder->current_image = NULL;
831
832 // Free audio buffer
833 SAFE_FREE(decoder->audio_buffer);
834
835 // Free swscale context
836 if (decoder->sws_ctx) {
837 sws_freeContext(decoder->sws_ctx);
838 decoder->sws_ctx = NULL;
839 }
840
841 // Free swresample context
842 if (decoder->swr_ctx) {
843 swr_free(&decoder->swr_ctx);
844 }
845
846 // Free frame and packet
847 if (decoder->frame) {
848 av_frame_free(&decoder->frame);
849 }
850 if (decoder->packet) {
851 av_packet_free(&decoder->packet);
852 }
853
854 // Free codec contexts
855 if (decoder->video_codec_ctx) {
856 avcodec_free_context(&decoder->video_codec_ctx);
857 }
858 if (decoder->audio_codec_ctx) {
859 avcodec_free_context(&decoder->audio_codec_ctx);
860 }
861
862 // Free format context
863 if (decoder->format_ctx) {
864 avformat_close_input(&decoder->format_ctx);
865 }
866
867 // Free AVIO context (stdin only)
868 if (decoder->avio_ctx) {
869 av_freep(&decoder->avio_ctx->buffer);
870 avio_context_free(&decoder->avio_ctx);
871 }
872
873 SAFE_FREE(decoder);
874}
875
876/* ============================================================================
877 * Video Operations
878 * ============================================================================ */
879
881 if (!decoder || decoder->video_stream_idx < 0) {
882 return NULL;
883 }
884
885 // Try to get a prefetched frame from the background thread (preferred path)
886 mutex_lock(&decoder->prefetch_mutex);
887 if (decoder->prefetch_frame_ready && decoder->current_prefetch_image) {
888 // Release the previous buffer (rendering is now complete)
889 if (decoder->current_read_buffer == decoder->prefetch_image_a) {
890 decoder->buffer_a_in_use = false;
891 } else if (decoder->current_read_buffer == decoder->prefetch_image_b) {
892 decoder->buffer_b_in_use = false;
893 }
894
895 image_t *frame = decoder->current_prefetch_image;
896 decoder->prefetch_frame_ready = false;
897
898 // Mark the new buffer as in use (prevent prefetch thread from overwriting it during rendering)
899 if (frame == decoder->prefetch_image_a) {
900 decoder->buffer_a_in_use = true;
901 } else if (frame == decoder->prefetch_image_b) {
902 decoder->buffer_b_in_use = true;
903 }
904
905 decoder->current_read_buffer = frame;
906 mutex_unlock(&decoder->prefetch_mutex);
907
908 // Use the prefetched frame
909 decoder->current_image = frame;
910 log_dev_every(5 * US_PER_SEC_INT, "Using prefetched frame");
911 return frame;
912 }
913 mutex_unlock(&decoder->prefetch_mutex);
914
915 // No fallback synchronous decode - rely on background prefetch thread
916 // Skipping frames when prefetch not ready allows audio timing to advance
917 // This is critical for proper audio-video sync when prefetch is active
918 log_dev_every(5 * US_PER_SEC_INT,
919 "Prefetch frame not ready, skipping to next iteration (allow prefetch to catch up)");
920 return NULL;
921}
922
931 if (!decoder || decoder->video_stream_idx < 0) {
932 return ERROR_INVALID_PARAM;
933 }
934
935 if (!decoder->prefetch_image_a || !decoder->prefetch_image_b) {
936 return ERROR_INVALID_PARAM;
937 }
938
939 // Already running
940 if (decoder->prefetch_thread_running) {
941 return ASCIICHAT_OK;
942 }
943
944 // Reset stop flag and create thread
945 decoder->prefetch_should_stop = false;
946
947 int thread_err = asciichat_thread_create(&decoder->prefetch_thread, ffmpeg_decoder_prefetch_thread_func, decoder);
948 if (thread_err != 0) {
949 return SET_ERRNO(ERROR_THREAD, "Failed to create video prefetch thread");
950 }
951
952 decoder->prefetch_thread_running = true;
953 return ASCIICHAT_OK;
954}
955
960 if (!decoder || !decoder->prefetch_thread_running) {
961 return;
962 }
963
964 decoder->prefetch_should_stop = true;
965 // Wait up to 2 seconds for thread to stop
966 // The interrupt callback should cause av_read_frame to abort quickly
967 int join_result = asciichat_thread_join_timeout(&decoder->prefetch_thread, NULL, 2000 * NS_PER_MS_INT);
968
969 if (join_result == 0) {
970 // Thread exited successfully
971 decoder->prefetch_thread_running = false;
972 } else {
973 // Timeout: thread is still running (blocked on I/O)
974 // Mark as stopped anyway - we'll create a new thread on restart
975 // The old thread will eventually finish and exit
976 decoder->prefetch_thread_running = false;
977 }
978}
979
981 if (!decoder) {
982 return false;
983 }
984 return decoder->prefetch_thread_running;
985}
986
988 return decoder && decoder->video_stream_idx >= 0;
989}
990
991asciichat_error_t ffmpeg_decoder_get_video_dimensions(ffmpeg_decoder_t *decoder, int *width, int *height) {
992 if (!decoder || decoder->video_stream_idx < 0) {
993 return ERROR_INVALID_PARAM;
994 }
995
996 if (width) {
997 *width = decoder->video_codec_ctx->width;
998 }
999 if (height) {
1000 *height = decoder->video_codec_ctx->height;
1001 }
1002
1003 return ASCIICHAT_OK;
1004}
1005
1007 if (!decoder || decoder->video_stream_idx < 0) {
1008 return -1.0;
1009 }
1010
1011 AVStream *stream = decoder->format_ctx->streams[decoder->video_stream_idx];
1012
1013 // Try avg_frame_rate first (average frame rate from entire stream)
1014 double fps = av_q2d_safe(stream->avg_frame_rate);
1015
1016 // Fallback to r_frame_rate if avg_frame_rate is invalid or zero
1017 // r_frame_rate is the "real" frame rate based on codec parameters
1018 // This is more reliable for YouTube videos and some video codecs
1019 if (fps <= 0.0) {
1020 fps = av_q2d_safe(stream->r_frame_rate);
1021 }
1022
1023 return fps;
1024}
1025
1026/* ============================================================================
1027 * Audio Operations
1028 * ============================================================================ */
1029
1030size_t ffmpeg_decoder_read_audio_samples(ffmpeg_decoder_t *decoder, float *buffer, size_t num_samples) {
1031 if (!decoder || decoder->audio_stream_idx < 0 || !buffer || num_samples == 0) {
1032 return 0;
1033 }
1034
1035 size_t samples_written = 0;
1036
1037 // First, drain any buffered samples
1038 if (decoder->audio_buffer_offset > 0) {
1039 size_t available = decoder->audio_buffer_offset;
1040 size_t to_copy = (available < num_samples) ? available : num_samples;
1041
1042 memcpy(buffer, decoder->audio_buffer, to_copy * sizeof(float));
1043 samples_written += to_copy;
1044
1045 // Shift buffer
1046 if (to_copy < available) {
1047 memmove(decoder->audio_buffer, decoder->audio_buffer + to_copy, (available - to_copy) * sizeof(float));
1048 }
1049 decoder->audio_buffer_offset -= to_copy;
1050
1051 if (samples_written >= num_samples) {
1052 return samples_written;
1053 }
1054 }
1055
1056 // Read more packets to fill the request
1057 static uint64_t packet_count = 0;
1058 while (samples_written < num_samples) {
1059 int ret = av_read_frame(decoder->format_ctx, decoder->packet);
1060 if (ret < 0) {
1061 if (ret == AVERROR_EOF) {
1062 decoder->eof_reached = true;
1063 }
1064 break;
1065 }
1066
1067 // Check if this is an audio packet
1068 if (decoder->packet->stream_index != decoder->audio_stream_idx) {
1069 av_packet_unref(decoder->packet);
1070 continue;
1071 }
1072
1073 log_info_every(50 * US_PER_MS_INT, "Audio packet #%lu: pts=%ld dts=%ld duration=%d size=%d", packet_count++,
1074 decoder->packet->pts, decoder->packet->dts, decoder->packet->duration, decoder->packet->size);
1075
1076 // Send packet to decoder
1077 ret = avcodec_send_packet(decoder->audio_codec_ctx, decoder->packet);
1078 av_packet_unref(decoder->packet);
1079
1080 if (ret < 0) {
1081 log_warn("Error sending audio packet to decoder");
1082 continue;
1083 }
1084
1085 // Receive all decoded frames from this packet
1086 // Important: a single packet can produce multiple frames. Must drain all before next packet.
1087 while (1) {
1088 ret = avcodec_receive_frame(decoder->audio_codec_ctx, decoder->frame);
1089 if (ret == AVERROR(EAGAIN)) {
1090 break; // No more frames from this packet, get next packet
1091 } else if (ret < 0) {
1092 log_warn("Error receiving audio frame from decoder");
1093 goto audio_read_done;
1094 }
1095
1096 // Update position tracking
1097 decoder->last_audio_pts =
1098 get_frame_pts_seconds(decoder->frame, decoder->format_ctx->streams[decoder->audio_stream_idx]->time_base);
1099
1100 // Resample to target format
1101 float *out_buf = buffer + samples_written;
1102 int out_samples = (int)(num_samples - samples_written);
1103
1104 uint8_t *out_ptr = (uint8_t *)out_buf;
1105 int converted = swr_convert(decoder->swr_ctx, &out_ptr, out_samples, (const uint8_t **)decoder->frame->data,
1106 decoder->frame->nb_samples);
1107
1108 if (converted > 0) {
1109 samples_written += (size_t)converted;
1110 }
1111
1112 if (samples_written >= num_samples) {
1113 goto audio_read_done;
1114 }
1115 }
1116 }
1117
1118audio_read_done:
1119 // Flush resampler buffer if we haven't filled the full request
1120 // The resampler may have buffered samples that need to be output
1121 if (samples_written < num_samples) {
1122 int remaining_space = (int)(num_samples - samples_written);
1123 uint8_t *out_ptr = (uint8_t *)(buffer + samples_written);
1124 int flushed = swr_convert(decoder->swr_ctx, &out_ptr, remaining_space, NULL, 0);
1125 if (flushed > 0) {
1126 samples_written += (size_t)flushed;
1127 }
1128 }
1129
1130 // Update sample-based position tracking
1131 decoder->audio_samples_read += samples_written;
1132
1133 return samples_written;
1134}
1135
1137 return decoder && decoder->audio_stream_idx >= 0;
1138}
1139
1140/* ============================================================================
1141 * Playback Control
1142 * ============================================================================ */
1143
1144asciichat_error_t ffmpeg_decoder_rewind(ffmpeg_decoder_t *decoder) {
1145 if (!decoder) {
1146 return ERROR_INVALID_PARAM;
1147 }
1148
1149 if (decoder->is_stdin) {
1150 return ERROR_NOT_SUPPORTED; // Cannot seek stdin
1151 }
1152
1153 // Flush codec buffers
1154 if (decoder->video_codec_ctx) {
1155 avcodec_flush_buffers(decoder->video_codec_ctx);
1156 }
1157 if (decoder->audio_codec_ctx) {
1158 avcodec_flush_buffers(decoder->audio_codec_ctx);
1159 }
1160
1161 // Seek to beginning
1162 if (av_seek_frame(decoder->format_ctx, -1, 0, AVSEEK_FLAG_BACKWARD) < 0) {
1163 return SET_ERRNO(ERROR_MEDIA_SEEK, "Failed to seek to beginning");
1164 }
1165
1166 decoder->eof_reached = false;
1167 decoder->audio_buffer_offset = 0;
1168 decoder->last_video_pts = -1.0;
1169 decoder->last_audio_pts = -1.0;
1170 decoder->audio_samples_read = 0; // Reset sample counter to 0
1171
1172 return ASCIICHAT_OK;
1173}
1174
1175asciichat_error_t ffmpeg_decoder_seek_to_timestamp(ffmpeg_decoder_t *decoder, double timestamp_sec) {
1176 if (!decoder) {
1177 return ERROR_INVALID_PARAM;
1178 }
1179
1180 if (decoder->is_stdin) {
1181 return ERROR_NOT_SUPPORTED; // Cannot seek stdin
1182 }
1183
1184 // Hold mutex during entire seek operation to prevent race with prefetch thread
1185 mutex_lock(&decoder->prefetch_mutex);
1186
1187 // Set flag to pause prefetch thread via condition variable
1188 decoder->seeking_in_progress = true;
1189
1190 // Convert seconds to FFmpeg time base units (AV_TIME_BASE = 1,000,000)
1191 int64_t target_ts = (int64_t)(timestamp_sec * AV_TIME_BASE);
1192
1193 // For HTTP streams, use simple keyframe seeking (faster than frame-accurate seeking)
1194 // HTTP seeking is expensive and can break stream state, so prefer speed over precision
1195 int seek_ret = av_seek_frame(decoder->format_ctx, -1, target_ts, AVSEEK_FLAG_BACKWARD);
1196 if (seek_ret < 0) {
1197 // Fallback: try without backward flag
1198 seek_ret = av_seek_frame(decoder->format_ctx, -1, target_ts, 0);
1199 }
1200
1201 if (seek_ret < 0) {
1202 decoder->seeking_in_progress = false;
1203 cond_signal(&decoder->prefetch_cond);
1204 mutex_unlock(&decoder->prefetch_mutex);
1205 return SET_ERRNO(ERROR_MEDIA_SEEK, "Failed to seek to timestamp %.2f seconds", timestamp_sec);
1206 }
1207
1208 // Flush codec buffers AFTER seeking
1209 if (decoder->video_codec_ctx) {
1210 avcodec_flush_buffers(decoder->video_codec_ctx);
1211 }
1212 if (decoder->audio_codec_ctx) {
1213 avcodec_flush_buffers(decoder->audio_codec_ctx);
1214 }
1215
1216 // Reset state
1217 decoder->eof_reached = false;
1218 decoder->audio_buffer_offset = 0;
1219 // Clear any stale audio data in buffer
1220 if (decoder->audio_buffer) {
1221 memset(decoder->audio_buffer, 0, decoder->audio_buffer_size * sizeof(float));
1222 }
1223 decoder->last_video_pts = -1.0;
1224 decoder->last_audio_pts = -1.0;
1225 decoder->prefetch_frame_ready = false;
1226 // Set audio_samples_read to match the seek target so position tracking works correctly
1227 decoder->audio_samples_read = (uint64_t)(timestamp_sec * decoder->audio_sample_rate);
1228
1229 // Reset current_read_buffer and mark both buffers as not in use
1230 // After seeking, the prefetch thread may reallocate buffers, so current_read_buffer
1231 // could point to freed memory. Clear it so the next read doesn't try to release stale pointers.
1232 decoder->current_read_buffer = NULL;
1233 decoder->buffer_a_in_use = false;
1234 decoder->buffer_b_in_use = false;
1235
1236 // Resume prefetch thread
1237 decoder->seeking_in_progress = false;
1238 cond_signal(&decoder->prefetch_cond);
1239
1240 // Release mutex - prefetch thread can resume
1241 mutex_unlock(&decoder->prefetch_mutex);
1242
1243 return ASCIICHAT_OK;
1244}
1245
1247 return decoder && decoder->eof_reached;
1248}
1249
1251 if (!decoder || !decoder->format_ctx) {
1252 return -1.0;
1253 }
1254
1255 if (decoder->format_ctx->duration == AV_NOPTS_VALUE) {
1256 return -1.0;
1257 }
1258
1259 return (double)decoder->format_ctx->duration / AV_TIME_BASE;
1260}
1261
1263 if (!decoder) {
1264 return -1.0;
1265 }
1266
1267 // Prefer sample-based position tracking (continuous, works before frames are decoded)
1268 if (decoder->audio_sample_rate > 0 && decoder->audio_samples_read >= 0) {
1269 return (double)decoder->audio_samples_read / (double)decoder->audio_sample_rate;
1270 }
1271
1272 // Fallback to frame-based position if available
1273 if (decoder->last_video_pts >= 0.0) {
1274 return decoder->last_video_pts;
1275 } else if (decoder->last_audio_pts >= 0.0) {
1276 return decoder->last_audio_pts;
1277 }
1278
1279 return -1.0;
1280}
bool ffmpeg_decoder_at_end(ffmpeg_decoder_t *decoder)
double ffmpeg_decoder_get_position(ffmpeg_decoder_t *decoder)
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)
double ffmpeg_decoder_get_video_fps(ffmpeg_decoder_t *decoder)
asciichat_error_t ffmpeg_decoder_rewind(ffmpeg_decoder_t *decoder)
asciichat_error_t ffmpeg_decoder_get_video_dimensions(ffmpeg_decoder_t *decoder, int *width, int *height)
bool ffmpeg_decoder_has_audio(ffmpeg_decoder_t *decoder)
#define AVIO_BUFFER_SIZE
bool ffmpeg_decoder_is_prefetch_running(ffmpeg_decoder_t *decoder)
asciichat_error_t ffmpeg_decoder_start_prefetch(ffmpeg_decoder_t *decoder)
Start the background frame prefetching thread.
asciichat_error_t ffmpeg_decoder_seek_to_timestamp(ffmpeg_decoder_t *decoder, double timestamp_sec)
#define TARGET_SAMPLE_RATE
void ffmpeg_decoder_destroy(ffmpeg_decoder_t *decoder)
ffmpeg_decoder_t * ffmpeg_decoder_create(const char *path)
size_t ffmpeg_decoder_read_audio_samples(ffmpeg_decoder_t *decoder, float *buffer, size_t num_samples)
ffmpeg_decoder_t * ffmpeg_decoder_create_stdin(void)
double ffmpeg_decoder_get_duration(ffmpeg_decoder_t *decoder)
void platform_sleep_us(unsigned int us)
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.
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.
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.
int mutex_init(mutex_t *mutex)
Definition threading.c:16
int asciichat_thread_create(asciichat_thread_t *thread, void *(*start_routine)(void *), void *arg)
Definition threading.c:42
int asciichat_thread_join(asciichat_thread_t *thread, void **retval)
Definition threading.c:46
int mutex_destroy(mutex_t *mutex)
Definition threading.c:21
bool url_is_valid(const char *url)
Definition url.c:81
uint64_t time_get_ns(void)
Definition util/time.c:48
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Definition util/time.c:90
void image_destroy(image_t *p)
Definition video/image.c:85
image_t * image_new(size_t width, size_t height)
Definition video/image.c:36