ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
Media

Files

file  ffmpeg_encoder.c
 FFmpeg video/image file encoder for render-file output.
 
file  renderer.c
 render_file_* — ties together font resolution, platform renderer, FFmpeg encoder
 
file  terminal.c
 Pixel renderer for render-file: libvterm + FreeType2 software compositor.
 
file  yt_dlp.c
 yt-dlp stream URL extraction implementation
 
file  codecs.h
 Video and audio codec definitions and capability bitmasks.
 
file  ffmpeg_decoder.h
 🎞️ FFmpeg-based media decoder for video and audio streams
 
file  ffmpeg_encoder.h
 FFmpeg video/image file encoder — codec selected from file extension.
 
file  renderer.h
 Terminal-to-pixel renderer and render-file output (audio + video)
 
file  source.h
 🎬 Unified media source abstraction for webcam, files, and stdin
 
file  yt_dlp.h
 yt-dlp stream URL extraction
 

Typedefs

typedef struct ffmpeg_decoder_t ffmpeg_decoder_t
 Opaque FFmpeg decoder handle.
 
typedef struct media_source_t media_source_t
 Opaque media source handle.
 

Enumerations

enum  media_source_type_t { MEDIA_SOURCE_WEBCAM , MEDIA_SOURCE_FILE , MEDIA_SOURCE_STDIN , MEDIA_SOURCE_TEST }
 Media source type enumeration. More...
 

Functions

ffmpeg_decoder_t * ffmpeg_decoder_create (const char *path)
 Create FFmpeg decoder from file path.
 
ffmpeg_decoder_t * ffmpeg_decoder_create_stdin (void)
 Create FFmpeg decoder from stdin.
 
void ffmpeg_decoder_destroy (ffmpeg_decoder_t *decoder)
 Destroy FFmpeg decoder and free resources.
 
asciichat_error_t ffmpeg_decoder_start_prefetch (ffmpeg_decoder_t *decoder)
 Start background frame prefetching thread.
 
void ffmpeg_decoder_stop_prefetch (ffmpeg_decoder_t *decoder)
 Stop background frame prefetching thread.
 
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.
 
bool ffmpeg_decoder_is_prefetch_running (ffmpeg_decoder_t *decoder)
 Check if background frame prefetching thread is running.
 
image_t * ffmpeg_decoder_read_video_frame (ffmpeg_decoder_t *decoder)
 Decode next video frame.
 
bool ffmpeg_decoder_has_video (ffmpeg_decoder_t *decoder)
 Check if decoder has video stream.
 
asciichat_error_t ffmpeg_decoder_get_video_dimensions (ffmpeg_decoder_t *decoder, int *width, int *height)
 Get video dimensions.
 
double ffmpeg_decoder_get_video_fps (ffmpeg_decoder_t *decoder)
 Get video frame rate.
 
size_t ffmpeg_decoder_read_audio_samples (ffmpeg_decoder_t *decoder, float *buffer, size_t num_samples)
 Decode audio samples.
 
bool ffmpeg_decoder_has_audio (ffmpeg_decoder_t *decoder)
 Check if decoder has audio stream.
 
asciichat_error_t ffmpeg_decoder_rewind (ffmpeg_decoder_t *decoder)
 Seek to beginning of media.
 
asciichat_error_t ffmpeg_decoder_seek_to_timestamp (ffmpeg_decoder_t *decoder, double timestamp_sec)
 Seek to specific timestamp in media.
 
bool ffmpeg_decoder_at_end (ffmpeg_decoder_t *decoder)
 Check if decoder reached end of stream.
 
double ffmpeg_decoder_get_duration (ffmpeg_decoder_t *decoder)
 Get media duration in seconds.
 
double ffmpeg_decoder_get_position (ffmpeg_decoder_t *decoder)
 Get current playback position in seconds.
 
media_source_t * media_source_create (media_source_type_t type, const char *path)
 Create a new media source.
 
void media_source_destroy (media_source_t *source)
 Destroy media source and free resources.
 
image_t * media_source_read_video (media_source_t *source)
 Read next video frame from media source.
 
bool media_source_has_video (media_source_t *source)
 Check if media source has video stream.
 
size_t media_source_read_audio (media_source_t *source, float *buffer, size_t num_samples)
 Read audio samples from media source.
 
bool media_source_has_audio (media_source_t *source)
 Check if media source has audio stream.
 
void media_source_set_loop (media_source_t *source, bool loop)
 Enable or disable looping.
 
void media_source_pause (media_source_t *source)
 Pause media playback.
 
void media_source_resume (media_source_t *source)
 Resume media playback after pause.
 
bool media_source_is_paused (media_source_t *source)
 Check if media source is paused.
 
void media_source_toggle_pause (media_source_t *source)
 Toggle pause state of media source.
 
bool media_source_at_end (media_source_t *source)
 Check if media source reached end of stream.
 
asciichat_error_t media_source_rewind (media_source_t *source)
 Rewind media source to beginning.
 
asciichat_error_t media_source_seek (media_source_t *source, double timestamp_sec)
 Seek media source to timestamp.
 
media_source_type_t media_source_get_type (media_source_t *source)
 Get media source type.
 
double media_source_get_duration (media_source_t *source)
 Get media duration in seconds.
 
double media_source_get_position (media_source_t *source)
 Get current playback position in seconds.
 
double media_source_get_video_fps (media_source_t *source)
 Get video frame rate in frames per second.
 
void media_source_set_audio_context (media_source_t *source, void *audio_ctx)
 Set audio context for playback buffer management.
 
void media_source_set_exit_callback (media_source_t *source, bool(*should_exit_callback)(void *), void *user_data)
 Set exit signal callback for graceful shutdown during I/O.
 
asciichat_error_t yt_dlp_extract_stream_url (const char *url, const char *yt_dlp_options, char *output_url, size_t output_size)
 Extract stream URL using yt-dlp with optional parameters.
 
bool yt_dlp_is_available (void)
 Check if yt-dlp is installed and accessible.
 

Video Codec Bitmask Constants

Bitmasks for video codec capabilities

Each codec is represented by a single bit, allowing multiple codecs to be advertised simultaneously in the codec_capabilities_video field.

#define VIDEO_CODEC_CAP_ALL   (VIDEO_CODEC_CAP_RGBA | VIDEO_CODEC_CAP_H265 | VIDEO_CODEC_CAP_JPEG)
 Default video codec capabilities (all codecs supported)
 

Audio Codec Bitmask Constants

Bitmasks for audio codec capabilities

Each codec is represented by a single bit, allowing multiple codecs to be advertised simultaneously in the codec_capabilities_audio field.

#define AUDIO_CODEC_CAP_ALL   (AUDIO_CODEC_CAP_RAW | AUDIO_CODEC_CAP_OPUS)
 Default audio codec capabilities (all codecs supported)
 
#define VIDEO_CODEC_SUPPORTED(cap, codec)   (((cap) & (1 << (codec))) != 0)
 Check if a video codec is supported in capabilities bitmask.
 
#define AUDIO_CODEC_SUPPORTED(cap, codec)   (((cap) & (1 << (codec))) != 0)
 Check if an audio codec is supported in capabilities bitmask.
 

Detailed Description

This header provides FFmpeg integration for decoding various media formats. It wraps FFmpeg's complex API into a simple interface for extracting RGB video frames and float audio samples.

CORE FEATURES:

FFMPEG LIBRARIES USED:

SUPPORTED CODECS:

Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

This header provides a unified interface for media sources in ascii-chat, abstracting over webcams, media files, and stdin input. This allows the capture thread to work with any media source transparently.

CORE FEATURES:

SUPPORTED SOURCES:

SUPPORTED FORMATS:

USAGE:

// Create media source from file
media_source_set_loop(source, true);
// Read video frames
while (!done) {
if (!frame) {
if (media_source_at_end(source)) break;
continue; // Error, try next frame
}
// Process frame...
}
// Read audio samples
float buffer[480];
size_t samples = media_source_read_audio(source, buffer, 480);
// Cleanup
bool media_source_at_end(media_source_t *source)
Check if media source reached end of stream.
Definition source.c:772
size_t media_source_read_audio(media_source_t *source, float *buffer, size_t num_samples)
Read audio samples from media source.
Definition source.c:652
void media_source_destroy(media_source_t *source)
Destroy media source and free resources.
Definition source.c:448
image_t * media_source_read_video(media_source_t *source)
Read next video frame from media source.
Definition source.c:523
media_source_t * media_source_create(media_source_type_t type, const char *path)
Create a new media source.
Definition source.c:187
void media_source_set_loop(media_source_t *source, bool loop)
Enable or disable looping.
Definition source.c:760
@ MEDIA_SOURCE_FILE
Media file (video/audio)
Definition source.h:83
int frame
Definition splash.c:99
Image structure.
Media source for video and audio capture.
Definition source.c:34
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

Macro Definition Documentation

◆ AUDIO_CODEC_CAP_ALL

#define AUDIO_CODEC_CAP_ALL   (AUDIO_CODEC_CAP_RAW | AUDIO_CODEC_CAP_OPUS)

#include <codecs.h>

Default audio codec capabilities (all codecs supported)

Definition at line 77 of file codecs.h.

◆ AUDIO_CODEC_SUPPORTED

#define AUDIO_CODEC_SUPPORTED (   cap,
  codec 
)    (((cap) & (1 << (codec))) != 0)

#include <codecs.h>

Check if an audio codec is supported in capabilities bitmask.

Parameters
capCodec capabilities bitmask (AUDIO_CODEC_CAP_*)
codecCodec type to check (audio_codec_t)
Returns
true if codec is supported, false otherwise

Definition at line 95 of file codecs.h.

◆ VIDEO_CODEC_CAP_ALL

#define VIDEO_CODEC_CAP_ALL   (VIDEO_CODEC_CAP_RGBA | VIDEO_CODEC_CAP_H265 | VIDEO_CODEC_CAP_JPEG)

#include <codecs.h>

Default video codec capabilities (all codecs supported)

Definition at line 50 of file codecs.h.

◆ VIDEO_CODEC_SUPPORTED

#define VIDEO_CODEC_SUPPORTED (   cap,
  codec 
)    (((cap) & (1 << (codec))) != 0)

#include <codecs.h>

Check if a video codec is supported in capabilities bitmask.

Parameters
capCodec capabilities bitmask (VIDEO_CODEC_CAP_*)
codecCodec type to check (video_codec_t)
Returns
true if codec is supported, false otherwise

Definition at line 86 of file codecs.h.

Typedef Documentation

◆ ffmpeg_decoder_t

#include <ffmpeg_decoder.h>

Opaque FFmpeg decoder handle.

Forward declaration of FFmpeg decoder structure. Use ffmpeg_decoder_create() to create and ffmpeg_decoder_destroy() to cleanup.

Definition at line 58 of file ffmpeg_decoder.h.

◆ media_source_t

#include <source.h>

Opaque media source handle.

Forward declaration of media source structure. Use media_source_create() to create and media_source_destroy() to cleanup.

THREAD SAFETY:

  • media_source_read_video() and media_source_read_audio() may be called from different threads (e.g., video render thread + audio callback thread)
  • Both operations access the shared FFmpeg decoder
  • FFmpeg decoders are NOT thread-safe for concurrent operations
  • media_source_t uses internal synchronization to protect decoder access
  • Callers do NOT need external locking

Definition at line 105 of file source.h.

Enumeration Type Documentation

◆ media_source_type_t

#include <source.h>

Media source type enumeration.

Identifies the type of media source being used.

Enumerator
MEDIA_SOURCE_WEBCAM 

Hardware webcam device.

MEDIA_SOURCE_FILE 

Media file (video/audio)

MEDIA_SOURCE_STDIN 

Piped or redirected input.

MEDIA_SOURCE_TEST 

Test pattern generator.

Definition at line 81 of file source.h.

81 {
media_source_type_t
Media source type enumeration.
Definition source.h:81
@ MEDIA_SOURCE_STDIN
Piped or redirected input.
Definition source.h:84
@ MEDIA_SOURCE_WEBCAM
Hardware webcam device.
Definition source.h:82
@ MEDIA_SOURCE_TEST
Test pattern generator.
Definition source.h:85

Function Documentation

◆ ffmpeg_decoder_at_end()

bool ffmpeg_decoder_at_end ( ffmpeg_decoder_t *  decoder)

#include <ffmpeg_decoder.h>

Check if decoder reached end of stream.

Parameters
decoderDecoder (must not be NULL)
Returns
true if EOF reached, false otherwise

Definition at line 1593 of file media/ffmpeg_decoder.c.

1593 {
1594 return decoder && decoder->eof_reached;
1595}
bool eof_reached
Whether end of file was reached.

References ffmpeg_decoder_t::eof_reached.

Referenced by media_source_at_end(), media_source_read_audio(), and media_source_read_video().

◆ ffmpeg_decoder_create()

ffmpeg_decoder_t * ffmpeg_decoder_create ( const char *  path)

#include <ffmpeg_decoder.h>

Create FFmpeg decoder from file path.

Parameters
pathFile path to media file (must not be NULL)
Returns
Pointer to decoder, or NULL on failure

Creates and initializes an FFmpeg decoder for the specified file. Opens the file, detects streams, and initializes codecs.

Initialization process:

  1. Open format context (avformat_open_input)
  2. Find stream information (avformat_find_stream_info)
  3. Detect video and audio streams
  4. Open codec contexts for detected streams
  5. Initialize swscale/swresample contexts
Note
Sets errno context on failure
Call ffmpeg_decoder_destroy() to cleanup

Definition at line 627 of file media/ffmpeg_decoder.c.

627 {
628 if (!path) {
629 SET_ERRNO(ERROR_INVALID_PARAM, "Path is NULL");
630 return NULL;
631 }
632
633 // Suppress FFmpeg's verbose debug logging (H.264 codec warnings, etc.)
634 // Only set this once, it's a global setting
635 static bool ffmpeg_log_level_set = false;
636 if (!ffmpeg_log_level_set) {
637 av_log_set_level(AV_LOG_QUIET); // Suppress all FFmpeg logging
638 av_log_set_callback(ffmpeg_silent_log_callback); // Install silent callback to discard all output
639 ffmpeg_log_level_set = true;
640 }
641
643 if (!decoder) {
644 SET_ERRNO(ERROR_MEMORY, "Failed to allocate decoder");
645 return NULL;
646 }
647
648 memset(decoder, 0, sizeof(*decoder));
649 decoder->video_stream_idx = -1;
650 decoder->audio_stream_idx = -1;
651 decoder->last_video_pts = -1.0;
652 decoder->last_audio_pts = -1.0;
653
654 // Capture FFmpeg's probing output (FFmpeg may write directly to either stream)
655 int ret = 0;
656 LOG_IO("ffmpeg", {
657 // Configure FFmpeg options for HTTP streaming performance
658 AVDictionary *options = NULL;
659
660 // For HTTP/HTTPS streams: enable fast probing and reconnection (validated via production-grade URL regex)
661 if (path && url_is_valid(path)) {
662 // Limit probing to 32KB for faster format detection
663 av_dict_set(&options, "probesize", "32768", 0);
664 // Analyze for 100ms max to determine streams quickly
665 av_dict_set(&options, "analyzeduration", "100000", 0);
666 // Enable auto-reconnection for interrupted connections
667 av_dict_set(&options, "reconnect", "1", 0);
668 // Allow reconnection for streamed protocols
669 av_dict_set(&options, "reconnect_streamed", "1", 0);
670 // Set reasonable I/O timeout (10 seconds)
671 av_dict_set(&options, "rw_timeout", "10000000", 0);
672 // Enable HTTP persistent connection (keep-alive) for better performance
673 av_dict_set(&options, "http_persistent", "1", 0);
674 // Reduce connect timeout to fail faster if server is unreachable
675 av_dict_set(&options, "connect_timeout", "5000000", 0);
676 }
677
678 // Open input file
679 ret = avformat_open_input(&decoder->format_ctx, path, NULL, &options);
680 av_dict_free(&options); // Free options dictionary
681
682 if (ret >= 0) {
683 // Find stream info
684 if (avformat_find_stream_info(decoder->format_ctx, NULL) < 0) {
685 ret = -1;
686 }
687 }
688 });
689
690 if (ret < 0) {
691 if (ret == -1) {
692 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to find stream info");
693 } else {
694 SET_ERRNO(ERROR_MEDIA_OPEN, "Failed to open media file: %s", path);
695 }
696 if (decoder->format_ctx) {
697 avformat_close_input(&decoder->format_ctx);
698 }
699 SAFE_FREE(decoder);
700 return NULL;
701 }
702
703 // Install interrupt callback to allow seeking to interrupt long av_read_frame() calls
704 // Do this AFTER finding stream info to ensure format context is fully initialized
705 if (decoder->format_ctx) {
706 decoder->format_ctx->interrupt_callback.callback = ffmpeg_interrupt_callback;
707 decoder->format_ctx->interrupt_callback.opaque = decoder;
708 }
709
710 // Open video codec
711 asciichat_error_t err = open_codec_context(decoder->format_ctx, AVMEDIA_TYPE_VIDEO, &decoder->video_stream_idx,
712 &decoder->video_codec_ctx);
713 if (err != ASCIICHAT_OK) {
714 log_warn("Failed to open video codec (file may be audio-only)");
715 }
716
717 // Open audio codec - audio is enabled by default (no option needed)
718 // Always try to open audio codec, don't rely on GET_OPTION(audio_enabled) which has a default issue
719 err = open_codec_context(decoder->format_ctx, AVMEDIA_TYPE_AUDIO, &decoder->audio_stream_idx,
720 &decoder->audio_codec_ctx);
721 if (err != ASCIICHAT_OK) {
722 log_debug("No audio codec found (file may be video-only or audio codec not available)");
723 decoder->audio_stream_idx = -1;
724 decoder->audio_codec_ctx = NULL;
725 }
726
727 // Require at least one stream
728 if (decoder->video_stream_idx < 0 && decoder->audio_stream_idx < 0) {
729 SET_ERRNO(ERROR_MEDIA_DECODE, "No video or audio streams found");
730 ffmpeg_decoder_destroy(decoder);
731 return NULL;
732 }
733
734 // Allocate frame and packet
735 decoder->frame = av_frame_alloc();
736 decoder->packet = av_packet_alloc();
737 if (!decoder->frame || !decoder->packet) {
738 SET_ERRNO(ERROR_MEMORY, "Failed to allocate frame/packet");
739 ffmpeg_decoder_destroy(decoder);
740 return NULL;
741 }
742
743 // Initialize swscale context for video if present
744 if (decoder->video_codec_ctx) {
745 // Validate codec context has valid dimensions and pixel format
746 // For HTTP streams, these might not be valid until first frame is read
747 if (decoder->video_codec_ctx->width <= 0 || decoder->video_codec_ctx->height <= 0) {
748 log_warn("Video codec has invalid dimensions (%dx%d), will initialize swscale on first frame",
749 decoder->video_codec_ctx->width, decoder->video_codec_ctx->height);
750 // Don't create swscale context yet - will create it lazily on first frame read
751 } else if (decoder->video_codec_ctx->pix_fmt == AV_PIX_FMT_NONE) {
752 log_warn("Video codec has invalid pixel format, will initialize swscale on first frame");
753 // Don't create swscale context yet - will create it lazily on first frame read
754 } else {
755 // Create swscale context with valid parameters
756 LOG_IO("swscaler", {
757 decoder->sws_ctx =
758 sws_getContext(decoder->video_codec_ctx->width, decoder->video_codec_ctx->height,
759 decoder->video_codec_ctx->pix_fmt, decoder->video_codec_ctx->width,
760 decoder->video_codec_ctx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
761 });
762 if (!decoder->sws_ctx) {
763 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to create swscale context");
764 ffmpeg_decoder_destroy(decoder);
765 return NULL;
766 }
767 }
768 }
769
770 // Initialize swresample context for audio if present
771 if (decoder->audio_codec_ctx) {
772 // Store output sample rate for position tracking
774
775 // Allocate resampler context
776 decoder->swr_ctx = swr_alloc();
777 if (!decoder->swr_ctx) {
778 SET_ERRNO(ERROR_MEMORY, "Failed to allocate swresample context");
779 ffmpeg_decoder_destroy(decoder);
780 return NULL;
781 }
782
783 // Set options
784 av_opt_set_chlayout(decoder->swr_ctx, "in_chlayout", &decoder->audio_codec_ctx->ch_layout, 0);
785 av_opt_set_int(decoder->swr_ctx, "in_sample_rate", decoder->audio_codec_ctx->sample_rate, 0);
786 av_opt_set_sample_fmt(decoder->swr_ctx, "in_sample_fmt", decoder->audio_codec_ctx->sample_fmt, 0);
787
788 AVChannelLayout out_ch_layout = AV_CHANNEL_LAYOUT_MONO;
789 av_opt_set_chlayout(decoder->swr_ctx, "out_chlayout", &out_ch_layout, 0);
790 av_opt_set_int(decoder->swr_ctx, "out_sample_rate", TARGET_SAMPLE_RATE, 0);
791 av_opt_set_sample_fmt(decoder->swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
792
793 // Initialize
794 if (swr_init(decoder->swr_ctx) < 0) {
795 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to initialize swresample context");
796 ffmpeg_decoder_destroy(decoder);
797 return NULL;
798 }
799
800 // Allocate audio buffer (10 seconds worth)
802 decoder->audio_buffer = SAFE_MALLOC(decoder->audio_buffer_size * sizeof(float), float *);
803 if (!decoder->audio_buffer) {
804 SET_ERRNO(ERROR_MEMORY, "Failed to allocate audio buffer");
805 ffmpeg_decoder_destroy(decoder);
806 return NULL;
807 }
808 }
809
810 // Initialize video frame prefetching system (for YouTube streaming)
811 if (decoder->video_stream_idx >= 0) {
812 int width = decoder->video_codec_ctx->width;
813 int height = decoder->video_codec_ctx->height;
814
815 // Create two prefetch image buffers for double-buffering
816 decoder->prefetch_image_a = image_new((size_t)width, (size_t)height);
817 decoder->prefetch_image_b = image_new((size_t)width, (size_t)height);
818 if (!decoder->prefetch_image_a || !decoder->prefetch_image_b) {
819 SET_ERRNO(ERROR_MEMORY, "Failed to allocate prefetch image buffers");
820 ffmpeg_decoder_destroy(decoder);
821 return NULL;
822 }
823
824 decoder->current_prefetch_image = decoder->prefetch_image_a;
825 decoder->prefetch_frame_ready = false;
826
827 // Initialize prefetch mutex
828 if (mutex_init(&decoder->prefetch_mutex, "ffmpeg_prefetch") != 0) {
829 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch mutex");
830 ffmpeg_decoder_destroy(decoder);
831 return NULL;
832 }
833
834 if (cond_init(&decoder->prefetch_cond, "ffmpeg_prefetch") != 0) {
835 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch condition variable");
836 mutex_destroy(&decoder->prefetch_mutex);
837 ffmpeg_decoder_destroy(decoder);
838 return NULL;
839 }
840
841 if (mutex_init(&decoder->read_frame_mutex, "ffmpeg_read_frame") != 0) {
842 SET_ERRNO(ERROR_MEMORY, "Failed to initialize read_frame mutex");
843 mutex_destroy(&decoder->prefetch_mutex);
844 cond_destroy(&decoder->prefetch_cond);
845 ffmpeg_decoder_destroy(decoder);
846 return NULL;
847 }
848
849 decoder->prefetch_thread_running = false;
850 decoder->prefetch_should_stop = false;
851 }
852
853 log_debug("FFmpeg decoder opened: %s (video=%s, audio=%s)", path, decoder->video_stream_idx >= 0 ? "yes" : "no",
854 decoder->audio_stream_idx >= 0 ? "yes" : "no");
855
856 char decoder_name[256];
857 snprintf(decoder_name, sizeof(decoder_name), "%s", path);
858 NAMED_REGISTER_FFMPEG_DECODER(decoder, decoder_name, NULL);
859
860 return decoder;
861}
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define NAMED_REGISTER_FFMPEG_DECODER(decoder, name, parent_ptr)
Register an FFmpeg decoder with automatic format specifier.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_MEDIA_DECODE
Definition error_codes.h:72
@ ERROR_INVALID_PARAM
@ ERROR_MEDIA_OPEN
Definition error_codes.h:71
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
void ffmpeg_decoder_destroy(ffmpeg_decoder_t *decoder)
Destroy FFmpeg decoder and free resources.
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
int cond_init(cond_t *cond, const char *name)
Initialize a condition variable with a name.
int cond_destroy(cond_t *cond)
Destroy a condition variable.
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
bool url_is_valid(const char *url)
Fast URL validation using production-grade regex (HTTP/HTTPS/WebSocket/TCP)
Definition url.c:81
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
#define LOG_IO(prefix, block)
Capture output from a code block and log it.
Definition io.h:81
#define TARGET_SAMPLE_RATE
FFmpeg decoder state for video and audio decoding.
mutex_t prefetch_mutex
Protect prefetch state and FFmpeg decoder access.
AVCodecContext * video_codec_ctx
Video codec context.
image_t * prefetch_image_a
First prefetch buffer.
AVFormatContext * format_ctx
FFmpeg format/container context.
bool prefetch_thread_running
Whether prefetch thread is active.
image_t * prefetch_image_b
Second prefetch buffer.
cond_t prefetch_cond
Condition variable for pausing during seek.
mutex_t read_frame_mutex
Protect av_read_frame() calls from video and audio threads.
struct SwrContext * swr_ctx
Software resampler for format conversion.
bool prefetch_frame_ready
Whether current_prefetch_image has valid data.
AVPacket * packet
Reusable packet for reading.
int video_stream_idx
Video stream index (-1 if none)
double last_audio_pts
Last audio presentation timestamp.
AVCodecContext * audio_codec_ctx
Audio codec context.
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.
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.
int audio_stream_idx
Audio stream index (-1 if none)
AVFrame * frame
Reusable frame for decoding.
struct SwsContext * sws_ctx
Software scaler for format conversion.

References ASCIICHAT_OK, ffmpeg_decoder_t::audio_buffer, ffmpeg_decoder_t::audio_buffer_size, ffmpeg_decoder_t::audio_codec_ctx, ffmpeg_decoder_t::audio_sample_rate, ffmpeg_decoder_t::audio_stream_idx, cond_destroy(), cond_init(), ffmpeg_decoder_t::current_prefetch_image, ERROR_INVALID_PARAM, ERROR_MEDIA_DECODE, ERROR_MEDIA_OPEN, ERROR_MEMORY, ffmpeg_decoder_destroy(), ffmpeg_decoder_t::format_ctx, ffmpeg_decoder_t::frame, image_new(), ffmpeg_decoder_t::last_audio_pts, ffmpeg_decoder_t::last_video_pts, log_debug, LOG_IO, log_warn, mutex_destroy(), mutex_init(), NAMED_REGISTER_FFMPEG_DECODER, ffmpeg_decoder_t::packet, ffmpeg_decoder_t::prefetch_cond, ffmpeg_decoder_t::prefetch_frame_ready, ffmpeg_decoder_t::prefetch_image_a, ffmpeg_decoder_t::prefetch_image_b, ffmpeg_decoder_t::prefetch_mutex, ffmpeg_decoder_t::prefetch_should_stop, ffmpeg_decoder_t::prefetch_thread_running, ffmpeg_decoder_t::read_frame_mutex, SAFE_FREE, SAFE_MALLOC, SET_ERRNO, ffmpeg_decoder_t::swr_ctx, ffmpeg_decoder_t::sws_ctx, TARGET_SAMPLE_RATE, url_is_valid(), ffmpeg_decoder_t::video_codec_ctx, and ffmpeg_decoder_t::video_stream_idx.

Referenced by media_source_create().

◆ ffmpeg_decoder_create_stdin()

ffmpeg_decoder_t * ffmpeg_decoder_create_stdin ( void  )

#include <ffmpeg_decoder.h>

Create FFmpeg decoder from stdin.

Returns
Pointer to decoder, or NULL on failure

Creates an FFmpeg decoder that reads from stdin using a custom AVIOContext. This allows piping media data directly into ascii-chat.

Usage:

cat video.mp4 | ascii-chat client --file -
ffmpeg -i input.avi -f matroska - | ascii-chat client --file -

Limitations:

  • Cannot seek (no loop support)
  • Some formats may not work well with stdin (require seekable input)
  • Recommend using formats designed for streaming (matroska, mpegts)
Note
Sets errno context on failure
Call ffmpeg_decoder_destroy() to cleanup

Definition at line 863 of file media/ffmpeg_decoder.c.

863 {
864 // Buffer entire stdin on first call (shared between video and audio decoders)
865 if (!g_stdin_buffer) {
866 log_debug("Reading stdin into buffer...");
867 g_stdin_buffer = stdin_buffer_read_all();
868 if (!g_stdin_buffer) {
869 SET_ERRNO(ERROR_MEDIA_OPEN, "Failed to read stdin into buffer");
870 return NULL;
871 }
872 }
873
875 if (!decoder) {
876 SET_ERRNO(ERROR_MEMORY, "Failed to allocate decoder");
877 return NULL;
878 }
879
880 memset(decoder, 0, sizeof(*decoder));
881 decoder->video_stream_idx = -1;
882 decoder->audio_stream_idx = -1;
883 decoder->is_stdin = true;
884 decoder->last_video_pts = -1.0;
885 decoder->last_audio_pts = -1.0;
886
887 // Create per-decoder reader (each decoder gets independent position tracking)
889 if (!reader) {
890 SET_ERRNO(ERROR_MEMORY, "Failed to allocate buffer reader");
891 SAFE_FREE(decoder);
892 return NULL;
893 }
894 reader->buffer = g_stdin_buffer;
895 reader->pos = 0; // Each decoder starts from position 0
896
897 // AVIO takes ownership of this buffer and may resize or free it with av_free().
898 decoder->avio_buffer = av_malloc(AVIO_BUFFER_SIZE);
899 if (!decoder->avio_buffer) {
900 SET_ERRNO(ERROR_MEMORY, "Failed to allocate AVIO buffer");
901 SAFE_FREE(reader);
902 SAFE_FREE(decoder);
903 return NULL;
904 }
905
906 decoder->avio_ctx = avio_alloc_context(decoder->avio_buffer, // internal buffer for AVIO
907 AVIO_BUFFER_SIZE, // buffer size
908 0, // write_flag
909 reader, // opaque (per-decoder reader)
910 memory_read_packet,
911 NULL, // write_packet
912 memory_seek_packet // seek (memory is seekable)
913 );
914
915 if (!decoder->avio_ctx) {
916 SET_ERRNO(ERROR_MEMORY, "Failed to create AVIO context");
917 av_free(decoder->avio_buffer);
918 decoder->avio_buffer = NULL;
919 SAFE_FREE(decoder);
920 return NULL;
921 }
922
923 // Mark AVIO context as seekable (memory buffer supports seeking)
924 decoder->avio_ctx->seekable = AVIO_SEEKABLE_NORMAL;
925
926 // Allocate format context
927 decoder->format_ctx = avformat_alloc_context();
928 if (!decoder->format_ctx) {
929 SET_ERRNO(ERROR_MEMORY, "Failed to allocate format context");
930 av_freep(&decoder->avio_ctx->buffer);
931 avio_context_free(&decoder->avio_ctx);
932 SAFE_FREE(decoder);
933 return NULL;
934 }
935
936 decoder->format_ctx->pb = decoder->avio_ctx;
937 decoder->format_ctx->flags |= AVFMT_FLAG_CUSTOM_IO; // Tell FFmpeg to use our custom AVIO context
938
939 // Capture FFmpeg's probing output
940 int ret = 0;
941 LOG_IO("ffmpeg", {
942 // Open input from buffered stdin
943 ret = avformat_open_input(&decoder->format_ctx, NULL, NULL, NULL);
944 if (ret >= 0) {
945 // Find stream info
946 if (avformat_find_stream_info(decoder->format_ctx, NULL) < 0) {
947 ret = -1;
948 }
949 }
950 });
951
952 if (ret < 0) {
953 if (ret == -1) {
954 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to find stream info from stdin buffer");
955 } else {
956 SET_ERRNO(ERROR_MEDIA_OPEN, "Failed to open stdin buffer");
957 }
958 av_freep(&decoder->avio_ctx->buffer);
959 avio_context_free(&decoder->avio_ctx);
960 if (decoder->format_ctx) {
961 avformat_free_context(decoder->format_ctx);
962 }
963 SAFE_FREE(decoder);
964 return NULL;
965 }
966
967 // Open codecs (same as file-based decoder)
968 asciichat_error_t err = open_codec_context(decoder->format_ctx, AVMEDIA_TYPE_VIDEO, &decoder->video_stream_idx,
969 &decoder->video_codec_ctx);
970 if (err != ASCIICHAT_OK) {
971 log_warn("Failed to open video codec from stdin");
972 }
973
974 if (GET_OPTION(audio_enabled)) {
975 err = open_codec_context(decoder->format_ctx, AVMEDIA_TYPE_AUDIO, &decoder->audio_stream_idx,
976 &decoder->audio_codec_ctx);
977 if (err != ASCIICHAT_OK) {
978 log_warn("Failed to open audio codec from stdin");
979 }
980 } else {
981 decoder->audio_stream_idx = -1;
982 decoder->audio_codec_ctx = NULL;
983 log_debug("Audio decoding disabled by user option");
984 }
985
986 if (decoder->video_stream_idx < 0 && decoder->audio_stream_idx < 0) {
987 SET_ERRNO(ERROR_MEDIA_DECODE, "No video or audio streams found in stdin");
988 ffmpeg_decoder_destroy(decoder);
989 return NULL;
990 }
991
992 // Allocate frame and packet
993 decoder->frame = av_frame_alloc();
994 decoder->packet = av_packet_alloc();
995 if (!decoder->frame || !decoder->packet) {
996 SET_ERRNO(ERROR_MEMORY, "Failed to allocate frame/packet");
997 ffmpeg_decoder_destroy(decoder);
998 return NULL;
999 }
1000
1001 // Initialize swscale/swresample (same as file-based)
1002 if (decoder->video_codec_ctx) {
1003 // Validate codec context has valid dimensions and pixel format
1004 // For stdin/HTTP streams, these might not be valid until first frame is read
1005 if (decoder->video_codec_ctx->width <= 0 || decoder->video_codec_ctx->height <= 0) {
1006 log_warn("Video codec has invalid dimensions (%dx%d), will initialize swscale on first frame",
1007 decoder->video_codec_ctx->width, decoder->video_codec_ctx->height);
1008 // Don't create swscale context yet - will create it lazily on first frame read
1009 } else if (decoder->video_codec_ctx->pix_fmt == AV_PIX_FMT_NONE) {
1010 log_warn("Video codec has invalid pixel format, will initialize swscale on first frame");
1011 // Don't create swscale context yet - will create it lazily on first frame read
1012 } else {
1013 // Create swscale context with valid parameters
1014 LOG_IO("swscaler", {
1015 decoder->sws_ctx =
1016 sws_getContext(decoder->video_codec_ctx->width, decoder->video_codec_ctx->height,
1017 decoder->video_codec_ctx->pix_fmt, decoder->video_codec_ctx->width,
1018 decoder->video_codec_ctx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
1019 });
1020 if (!decoder->sws_ctx) {
1021 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to create swscale context");
1022 ffmpeg_decoder_destroy(decoder);
1023 return NULL;
1024 }
1025 }
1026 }
1027
1028 if (decoder->audio_codec_ctx) {
1029 decoder->swr_ctx = swr_alloc();
1030 if (!decoder->swr_ctx) {
1031 SET_ERRNO(ERROR_MEMORY, "Failed to allocate swresample context");
1032 ffmpeg_decoder_destroy(decoder);
1033 return NULL;
1034 }
1035
1036 av_opt_set_chlayout(decoder->swr_ctx, "in_chlayout", &decoder->audio_codec_ctx->ch_layout, 0);
1037 av_opt_set_int(decoder->swr_ctx, "in_sample_rate", decoder->audio_codec_ctx->sample_rate, 0);
1038 av_opt_set_sample_fmt(decoder->swr_ctx, "in_sample_fmt", decoder->audio_codec_ctx->sample_fmt, 0);
1039
1040 AVChannelLayout out_ch_layout = AV_CHANNEL_LAYOUT_MONO;
1041 av_opt_set_chlayout(decoder->swr_ctx, "out_chlayout", &out_ch_layout, 0);
1042 av_opt_set_int(decoder->swr_ctx, "out_sample_rate", TARGET_SAMPLE_RATE, 0);
1043 av_opt_set_sample_fmt(decoder->swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
1044
1045 if (swr_init(decoder->swr_ctx) < 0) {
1046 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to initialize swresample context");
1047 ffmpeg_decoder_destroy(decoder);
1048 return NULL;
1049 }
1050
1051 decoder->audio_buffer_size = TARGET_SAMPLE_RATE * 10;
1052 decoder->audio_buffer = SAFE_MALLOC(decoder->audio_buffer_size * sizeof(float), float *);
1053 if (!decoder->audio_buffer) {
1054 SET_ERRNO(ERROR_MEMORY, "Failed to allocate audio buffer");
1055 ffmpeg_decoder_destroy(decoder);
1056 return NULL;
1057 }
1058 }
1059
1060 // Initialize video frame prefetching system for stdin (same as file-based)
1061 if (decoder->video_stream_idx >= 0) {
1062 int width = decoder->video_codec_ctx->width;
1063 int height = decoder->video_codec_ctx->height;
1064
1065 // For stdin sources, dimensions might be invalid initially (0x0)
1066 // Use default dimensions for prefetch buffers; they'll be resized on first frame
1067 if (width <= 0 || height <= 0) {
1068 width = 1920; // Default width for stdin
1069 height = 1080; // Default height for stdin
1070 log_debug("stdin: Using default prefetch dimensions %dx%d (will be resized on first frame)", width, height);
1071 }
1072
1073 // Create two prefetch image buffers for double-buffering
1074 decoder->prefetch_image_a = image_new((size_t)width, (size_t)height);
1075 decoder->prefetch_image_b = image_new((size_t)width, (size_t)height);
1076 if (!decoder->prefetch_image_a || !decoder->prefetch_image_b) {
1077 SET_ERRNO(ERROR_MEMORY, "Failed to allocate prefetch image buffers");
1078 ffmpeg_decoder_destroy(decoder);
1079 return NULL;
1080 }
1081
1082 decoder->current_prefetch_image = decoder->prefetch_image_a;
1083 decoder->prefetch_frame_ready = false;
1084
1085 // Initialize prefetch mutex
1086 if (mutex_init(&decoder->prefetch_mutex, "ffmpeg_prefetch") != 0) {
1087 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch mutex");
1088 ffmpeg_decoder_destroy(decoder);
1089 return NULL;
1090 }
1091
1092 if (cond_init(&decoder->prefetch_cond, "ffmpeg_prefetch") != 0) {
1093 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch condition variable");
1094 mutex_destroy(&decoder->prefetch_mutex);
1095 ffmpeg_decoder_destroy(decoder);
1096 return NULL;
1097 }
1098
1099 if (mutex_init(&decoder->read_frame_mutex, "ffmpeg_read_frame") != 0) {
1100 SET_ERRNO(ERROR_MEMORY, "Failed to initialize read_frame mutex");
1101 mutex_destroy(&decoder->prefetch_mutex);
1102 cond_destroy(&decoder->prefetch_cond);
1103 ffmpeg_decoder_destroy(decoder);
1104 return NULL;
1105 }
1106 }
1107
1108 log_debug("FFmpeg decoder opened from stdin (video=%s, audio=%s)", decoder->video_stream_idx >= 0 ? "yes" : "no",
1109 decoder->audio_stream_idx >= 0 ? "yes" : "no");
1110
1111 NAMED_REGISTER_FFMPEG_DECODER(decoder, "stdin", NULL);
1112
1113 return decoder;
1114}
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
#define AVIO_BUFFER_SIZE
AVIOContext * avio_ctx
Custom I/O context for stdin.
unsigned char * avio_buffer
Buffer for custom I/O.
bool is_stdin
Whether reading from stdin.
size_t pos
Current read position (per-decoder)
stdin_buffer_t * buffer
Pointer to shared buffer.

References ASCIICHAT_OK, ffmpeg_decoder_t::audio_buffer, ffmpeg_decoder_t::audio_buffer_size, ffmpeg_decoder_t::audio_codec_ctx, ffmpeg_decoder_t::audio_stream_idx, ffmpeg_decoder_t::avio_buffer, AVIO_BUFFER_SIZE, ffmpeg_decoder_t::avio_ctx, stdin_buffer_reader_t::buffer, cond_destroy(), cond_init(), ffmpeg_decoder_t::current_prefetch_image, ERROR_MEDIA_DECODE, ERROR_MEDIA_OPEN, ERROR_MEMORY, ffmpeg_decoder_destroy(), ffmpeg_decoder_t::format_ctx, ffmpeg_decoder_t::frame, GET_OPTION, image_new(), ffmpeg_decoder_t::is_stdin, ffmpeg_decoder_t::last_audio_pts, ffmpeg_decoder_t::last_video_pts, log_debug, LOG_IO, log_warn, mutex_destroy(), mutex_init(), NAMED_REGISTER_FFMPEG_DECODER, ffmpeg_decoder_t::packet, stdin_buffer_reader_t::pos, ffmpeg_decoder_t::prefetch_cond, ffmpeg_decoder_t::prefetch_frame_ready, ffmpeg_decoder_t::prefetch_image_a, ffmpeg_decoder_t::prefetch_image_b, ffmpeg_decoder_t::prefetch_mutex, ffmpeg_decoder_t::read_frame_mutex, SAFE_FREE, SAFE_MALLOC, SET_ERRNO, ffmpeg_decoder_t::swr_ctx, ffmpeg_decoder_t::sws_ctx, TARGET_SAMPLE_RATE, ffmpeg_decoder_t::video_codec_ctx, and ffmpeg_decoder_t::video_stream_idx.

Referenced by media_source_create().

◆ ffmpeg_decoder_destroy()

void ffmpeg_decoder_destroy ( ffmpeg_decoder_t *  decoder)

#include <ffmpeg_decoder.h>

Destroy FFmpeg decoder and free resources.

Parameters
decoderDecoder to destroy (can be NULL)

Cleans up FFmpeg decoder and releases all resources. Safe to call with NULL. Automatically stops the background prefetch thread if running.

Cleanup process:

  1. Stop prefetch thread if running
  2. Free video frame ringbuffer
  3. Free swscale/swresample contexts
  4. Close codec contexts
  5. Close format context
  6. Free frame and packet structures
  7. Free decoder structure

Definition at line 1116 of file media/ffmpeg_decoder.c.

1116 {
1117 if (!decoder) {
1118 return;
1119 }
1120
1121 NAMED_UNREGISTER(decoder);
1122
1123 // Stop prefetch thread (signal it to stop and wait for it to finish)
1124 if (decoder->prefetch_thread_running) {
1125 mutex_lock(&decoder->prefetch_mutex);
1126 decoder->prefetch_should_stop = true;
1127 mutex_unlock(&decoder->prefetch_mutex);
1128
1129 // Wait for thread to finish
1130 asciichat_thread_join(&decoder->prefetch_thread, NULL);
1131 decoder->prefetch_thread_running = false;
1132 }
1133
1134 // Clean up prefetch state
1135 cond_destroy(&decoder->prefetch_cond);
1136 mutex_destroy(&decoder->prefetch_mutex);
1138
1139 // Free prefetch image buffers
1140 if (decoder->prefetch_image_a) {
1142 decoder->prefetch_image_a = NULL;
1143 }
1144 if (decoder->prefetch_image_b) {
1146 decoder->prefetch_image_b = NULL;
1147 }
1148
1149 // Don't destroy current_image - it points to one of the prefetch buffers
1150 // which have already been destroyed above
1151 decoder->current_image = NULL;
1152
1153 // Free audio buffer
1154 SAFE_FREE(decoder->audio_buffer);
1155
1156 // Free swscale context
1157 if (decoder->sws_ctx) {
1158 sws_freeContext(decoder->sws_ctx);
1159 decoder->sws_ctx = NULL;
1160 }
1161
1162 // Free swresample context
1163 if (decoder->swr_ctx) {
1164 swr_free(&decoder->swr_ctx);
1165 }
1166
1167 // Free frame and packet
1168 if (decoder->frame) {
1169 av_frame_free(&decoder->frame);
1170 }
1171 if (decoder->packet) {
1172 av_packet_free(&decoder->packet);
1173 }
1174
1175 // Free codec contexts
1176 if (decoder->video_codec_ctx) {
1177 avcodec_free_context(&decoder->video_codec_ctx);
1178 }
1179 if (decoder->audio_codec_ctx) {
1180 avcodec_free_context(&decoder->audio_codec_ctx);
1181 }
1182
1183 // Free format context
1184 if (decoder->format_ctx) {
1185 avformat_close_input(&decoder->format_ctx);
1186 }
1187
1188 // Free AVIO context (stdin only)
1189 if (decoder->avio_ctx) {
1190 // Free the opaque reader for stdin
1191 if (decoder->is_stdin) {
1192 SAFE_FREE(decoder->avio_ctx->opaque);
1193 }
1194 av_freep(&decoder->avio_ctx->buffer);
1195 avio_context_free(&decoder->avio_ctx);
1196 }
1197
1198 SAFE_FREE(decoder);
1199}
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
#define asciichat_thread_join(thread, timeout_ms)
asciichat_thread_t prefetch_thread
Prefetch thread handle.
image_t * current_image
Working buffer for decoding.

References asciichat_thread_join, ffmpeg_decoder_t::audio_buffer, ffmpeg_decoder_t::audio_codec_ctx, ffmpeg_decoder_t::avio_ctx, cond_destroy(), ffmpeg_decoder_t::current_image, ffmpeg_decoder_t::format_ctx, ffmpeg_decoder_t::frame, image_destroy(), ffmpeg_decoder_t::is_stdin, mutex_destroy(), mutex_lock, mutex_unlock, NAMED_UNREGISTER, ffmpeg_decoder_t::packet, ffmpeg_decoder_t::prefetch_cond, ffmpeg_decoder_t::prefetch_image_a, ffmpeg_decoder_t::prefetch_image_b, ffmpeg_decoder_t::prefetch_mutex, ffmpeg_decoder_t::prefetch_should_stop, ffmpeg_decoder_t::prefetch_thread, ffmpeg_decoder_t::prefetch_thread_running, ffmpeg_decoder_t::read_frame_mutex, SAFE_FREE, ffmpeg_decoder_t::swr_ctx, ffmpeg_decoder_t::sws_ctx, and ffmpeg_decoder_t::video_codec_ctx.

Referenced by ffmpeg_decoder_create(), ffmpeg_decoder_create_stdin(), media_source_create(), and media_source_destroy().

◆ ffmpeg_decoder_get_duration()

double ffmpeg_decoder_get_duration ( ffmpeg_decoder_t *  decoder)

#include <ffmpeg_decoder.h>

Get media duration in seconds.

Parameters
decoderDecoder (must not be NULL)
Returns
Duration in seconds, or -1.0 if unknown

Returns the total duration from the container metadata.

Definition at line 1597 of file media/ffmpeg_decoder.c.

1597 {
1598 if (!decoder || !decoder->format_ctx) {
1599 return -1.0;
1600 }
1601
1602 if (decoder->format_ctx->duration == AV_NOPTS_VALUE) {
1603 return -1.0;
1604 }
1605
1606 return (double)decoder->format_ctx->duration / AV_TIME_BASE;
1607}

References ffmpeg_decoder_t::format_ctx.

Referenced by media_source_get_duration().

◆ ffmpeg_decoder_get_position()

double ffmpeg_decoder_get_position ( ffmpeg_decoder_t *  decoder)

#include <ffmpeg_decoder.h>

Get current playback position in seconds.

Parameters
decoderDecoder (must not be NULL)
Returns
Current position in seconds, or -1.0 if unknown

Returns the presentation timestamp of the last decoded frame.

Definition at line 1609 of file media/ffmpeg_decoder.c.

1609 {
1610 if (!decoder) {
1611 return -1.0;
1612 }
1613
1614 // Prefer sample-based position tracking (continuous, works before frames are decoded)
1615 if (decoder->audio_sample_rate > 0 && decoder->audio_samples_read >= 0) {
1616 return (double)decoder->audio_samples_read / (double)decoder->audio_sample_rate;
1617 }
1618
1619 // Fallback to frame-based position if available
1620 if (decoder->last_video_pts >= 0.0) {
1621 return decoder->last_video_pts;
1622 } else if (decoder->last_audio_pts >= 0.0) {
1623 return decoder->last_audio_pts;
1624 }
1625
1626 return -1.0;
1627}
uint64_t audio_samples_read
Total audio samples decoded and output.

References ffmpeg_decoder_t::audio_sample_rate, ffmpeg_decoder_t::audio_samples_read, ffmpeg_decoder_t::last_audio_pts, and ffmpeg_decoder_t::last_video_pts.

Referenced by media_source_get_position(), media_source_read_audio(), and media_source_seek().

◆ ffmpeg_decoder_get_video_dimensions()

asciichat_error_t ffmpeg_decoder_get_video_dimensions ( ffmpeg_decoder_t *  decoder,
int *  width,
int *  height 
)

#include <ffmpeg_decoder.h>

Get video dimensions.

Parameters
decoderDecoder (must not be NULL)
widthOutput pointer for width (can be NULL)
heightOutput pointer for height (can be NULL)
Returns
ASCIICHAT_OK on success, error code if no video stream

Definition at line 1320 of file media/ffmpeg_decoder.c.

1320 {
1321 if (!decoder || decoder->video_stream_idx < 0) {
1322 return ERROR_INVALID_PARAM;
1323 }
1324
1325 if (width) {
1326 *width = decoder->video_codec_ctx->width;
1327 }
1328 if (height) {
1329 *height = decoder->video_codec_ctx->height;
1330 }
1331
1332 return ASCIICHAT_OK;
1333}

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ffmpeg_decoder_t::video_codec_ctx, and ffmpeg_decoder_t::video_stream_idx.

◆ ffmpeg_decoder_get_video_fps()

double ffmpeg_decoder_get_video_fps ( ffmpeg_decoder_t *  decoder)

#include <ffmpeg_decoder.h>

Get video frame rate.

Parameters
decoderDecoder (must not be NULL)
Returns
Frame rate in FPS, or -1.0 if unknown/no video

Returns the average frame rate of the video stream.

Definition at line 1335 of file media/ffmpeg_decoder.c.

1335 {
1336 if (!decoder || decoder->video_stream_idx < 0) {
1337 return -1.0;
1338 }
1339
1340 AVStream *stream = decoder->format_ctx->streams[decoder->video_stream_idx];
1341
1342 // Try avg_frame_rate first (average frame rate from entire stream)
1343 double fps = av_q2d_safe(stream->avg_frame_rate);
1344
1345 // Fallback to r_frame_rate if avg_frame_rate is invalid or zero
1346 // r_frame_rate is the "real" frame rate based on codec parameters
1347 // This is more reliable for YouTube videos and some video codecs
1348 if (fps <= 0.0) {
1349 fps = av_q2d_safe(stream->r_frame_rate);
1350 }
1351
1352 return fps;
1353}

References ffmpeg_decoder_t::format_ctx, and ffmpeg_decoder_t::video_stream_idx.

Referenced by media_source_get_video_fps().

◆ ffmpeg_decoder_has_audio()

bool ffmpeg_decoder_has_audio ( ffmpeg_decoder_t *  decoder)

#include <ffmpeg_decoder.h>

Check if decoder has audio stream.

Parameters
decoderDecoder (must not be NULL)
Returns
true if audio stream exists, false otherwise

Definition at line 1481 of file media/ffmpeg_decoder.c.

1481 {
1482 return decoder && decoder->audio_stream_idx >= 0;
1483}

References ffmpeg_decoder_t::audio_stream_idx.

Referenced by media_source_create(), and media_source_has_audio().

◆ ffmpeg_decoder_has_video()

bool ffmpeg_decoder_has_video ( ffmpeg_decoder_t *  decoder)

#include <ffmpeg_decoder.h>

Check if decoder has video stream.

Parameters
decoderDecoder (must not be NULL)
Returns
true if video stream exists, false otherwise

Definition at line 1316 of file media/ffmpeg_decoder.c.

1316 {
1317 return decoder && decoder->video_stream_idx >= 0;
1318}

References ffmpeg_decoder_t::video_stream_idx.

Referenced by media_source_create(), and media_source_has_video().

◆ ffmpeg_decoder_is_prefetch_running()

bool ffmpeg_decoder_is_prefetch_running ( ffmpeg_decoder_t *  decoder)

#include <ffmpeg_decoder.h>

Check if background frame prefetching thread is running.

Parameters
decoderDecoder (must not be NULL)
Returns
true if prefetch thread is running, false otherwise

Utility function to check if the prefetch thread is currently active. Used during seeking to determine if thread needs to be stopped/restarted.

Definition at line 1309 of file media/ffmpeg_decoder.c.

1309 {
1310 if (!decoder) {
1311 return false;
1312 }
1313 return decoder->prefetch_thread_running;
1314}

References ffmpeg_decoder_t::prefetch_thread_running.

◆ ffmpeg_decoder_read_audio_samples()

size_t ffmpeg_decoder_read_audio_samples ( ffmpeg_decoder_t *  decoder,
float *  buffer,
size_t  num_samples 
)

#include <ffmpeg_decoder.h>

Decode audio samples.

Parameters
decoderDecoder (must not be NULL)
bufferOutput buffer for samples (must not be NULL)
num_samplesNumber of samples to read
Returns
Number of samples actually read (0 to num_samples)

Decodes audio samples from the media stream. Samples are resampled to 48kHz mono float format, compatible with ascii-chat's Opus encoding.

Audio format:

  • Format: 32-bit float
  • Channels: Mono (1 channel)
  • Sample rate: 48000 Hz
  • Range: -1.0 to 1.0

Decoding process:

  1. Read packet from format context
  2. Send packet to audio decoder
  3. Receive decoded frame
  4. Resample to 48kHz mono using swresample
  5. Copy to output buffer
Note
Skips non-audio packets automatically
May return fewer samples than requested at EOF

Definition at line 1359 of file media/ffmpeg_decoder.c.

1359 {
1360 if (!decoder || decoder->audio_stream_idx < 0 || !buffer || num_samples == 0) {
1361 return 0;
1362 }
1363
1364 size_t samples_written = 0;
1365
1366 // First, drain any buffered samples
1367 if (decoder->audio_buffer_offset > 0) {
1368 size_t available = decoder->audio_buffer_offset;
1369 size_t to_copy = (available < num_samples) ? available : num_samples;
1370
1371 memcpy(buffer, decoder->audio_buffer, to_copy * sizeof(float));
1372 samples_written += to_copy;
1373
1374 // Shift buffer
1375 if (to_copy < available) {
1376 memmove(decoder->audio_buffer, decoder->audio_buffer + to_copy, (available - to_copy) * sizeof(float));
1377 }
1378 decoder->audio_buffer_offset -= to_copy;
1379
1380 if (samples_written >= num_samples) {
1381 decoder->audio_samples_read += samples_written;
1382 return samples_written;
1383 }
1384 }
1385
1386 // Read more packets to fill the request
1387 static uint64_t packet_count = 0;
1388
1389 // Lock read_frame_mutex to prevent concurrent av_read_frame() calls from video prefetch thread
1390 mutex_lock(&decoder->read_frame_mutex);
1391
1392 while (samples_written < num_samples) {
1393 int ret = av_read_frame(decoder->format_ctx, decoder->packet);
1394 if (ret < 0) {
1395 if (ret == AVERROR_EOF) {
1396 decoder->eof_reached = true;
1397 }
1398 break;
1399 }
1400
1401 // Check if this is an audio packet
1402 if (decoder->packet->stream_index != decoder->audio_stream_idx) {
1403 av_packet_unref(decoder->packet);
1404 continue;
1405 }
1406
1407 log_info_every(50 * US_PER_MS_INT, "Audio packet #%lu: pts=%ld dts=%ld duration=%d size=%d", packet_count++,
1408 decoder->packet->pts, decoder->packet->dts, decoder->packet->duration, decoder->packet->size);
1409
1410 // Send packet to decoder
1411 ret = avcodec_send_packet(decoder->audio_codec_ctx, decoder->packet);
1412 av_packet_unref(decoder->packet);
1413
1414 if (ret < 0) {
1415 char error_message[AV_ERROR_MAX_STRING_SIZE];
1416 av_strerror(ret, error_message, sizeof(error_message));
1417 log_warn("Error sending audio packet to decoder: %s", error_message);
1418 continue;
1419 }
1420
1421 // Receive all decoded frames from this packet
1422 // Important: a single packet can produce multiple frames. Must drain all before next packet.
1423 while (1) {
1424 ret = avcodec_receive_frame(decoder->audio_codec_ctx, decoder->frame);
1425 if (ret == AVERROR(EAGAIN)) {
1426 break; // No more frames from this packet, get next packet
1427 } else if (ret < 0) {
1428 log_warn("Error receiving audio frame from decoder");
1429 goto audio_read_done;
1430 }
1431
1432 // Update position tracking
1433 decoder->last_audio_pts =
1434 get_frame_pts_seconds(decoder->frame, decoder->format_ctx->streams[decoder->audio_stream_idx]->time_base);
1435
1436 // Resample the complete frame and retain output beyond this callback's
1437 // requested block. Flushing the resampler between packets would insert
1438 // padding into inputs whose sample rate differs from the output rate.
1439 uint8_t *out_ptr = (uint8_t *)decoder->audio_buffer;
1440 int converted = swr_convert(decoder->swr_ctx, &out_ptr, (int)decoder->audio_buffer_size,
1441 (const uint8_t **)decoder->frame->data, decoder->frame->nb_samples);
1442
1443 if (converted > 0) {
1444 size_t available = (size_t)converted;
1445 size_t remaining = num_samples - samples_written;
1446 size_t to_copy = available < remaining ? available : remaining;
1447 memcpy(buffer + samples_written, decoder->audio_buffer, to_copy * sizeof(float));
1448 samples_written += to_copy;
1449 decoder->audio_buffer_offset = available - to_copy;
1450 if (decoder->audio_buffer_offset > 0)
1451 memmove(decoder->audio_buffer, decoder->audio_buffer + to_copy, decoder->audio_buffer_offset * sizeof(float));
1452 }
1453
1454 if (samples_written >= num_samples) {
1455 goto audio_read_done;
1456 }
1457 }
1458 }
1459
1460audio_read_done:
1461 // Release read_frame_mutex now that av_read_frame() is done
1462 mutex_unlock(&decoder->read_frame_mutex);
1463
1464 // Flush resampler buffer if we haven't filled the full request
1465 // The resampler may have buffered samples that need to be output
1466 if (samples_written < num_samples && decoder->eof_reached) {
1467 int remaining_space = (int)(num_samples - samples_written);
1468 uint8_t *out_ptr = (uint8_t *)(buffer + samples_written);
1469 int flushed = swr_convert(decoder->swr_ctx, &out_ptr, remaining_space, NULL, 0);
1470 if (flushed > 0) {
1471 samples_written += (size_t)flushed;
1472 }
1473 }
1474
1475 // Update sample-based position tracking
1476 decoder->audio_samples_read += samples_written;
1477
1478 return samples_written;
1479}
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define US_PER_MS_INT
Definition time.h:160
#define EAGAIN
#define log_info_every(interval_us, fmt,...)
Rate-limited INFO logging.
Definition log/log.h:705
size_t audio_buffer_offset
Current offset in audio buffer.

References ffmpeg_decoder_t::audio_buffer, ffmpeg_decoder_t::audio_buffer_offset, ffmpeg_decoder_t::audio_buffer_size, ffmpeg_decoder_t::audio_codec_ctx, ffmpeg_decoder_t::audio_samples_read, ffmpeg_decoder_t::audio_stream_idx, EAGAIN, ffmpeg_decoder_t::eof_reached, ffmpeg_decoder_t::format_ctx, ffmpeg_decoder_t::frame, ffmpeg_decoder_t::last_audio_pts, log_info_every, log_warn, mutex_lock, mutex_unlock, ffmpeg_decoder_t::packet, ffmpeg_decoder_t::read_frame_mutex, ffmpeg_decoder_t::swr_ctx, and US_PER_MS_INT.

Referenced by media_source_read_audio().

◆ ffmpeg_decoder_read_video_frame()

image_t * ffmpeg_decoder_read_video_frame ( ffmpeg_decoder_t *  decoder)

#include <ffmpeg_decoder.h>

Decode next video frame.

Parameters
decoderDecoder (must not be NULL)
Returns
Pointer to image_t frame, or NULL on error/EOF

Decodes the next video frame from the media stream. Returns an image_t structure in RGB24 format.

Frame format:

  • Pixel format: RGB24 (8 bits per channel)
  • Dimensions: Original video dimensions
  • Memory: Owned by decoder (do NOT free)

Return values:

Decoding process:

  1. Read packet from format context
  2. Send packet to video decoder
  3. Receive decoded frame
  4. Convert frame to RGB24 using swscale
  5. Wrap in image_t structure
Note
Returned frame is valid until next call or decoder destruction
Skips non-video packets automatically

Definition at line 1205 of file media/ffmpeg_decoder.c.

1205 {
1206 if (!decoder || decoder->video_stream_idx < 0) {
1207 return NULL;
1208 }
1209
1210 // Try to get a prefetched frame from the background thread (preferred path)
1211 mutex_lock(&decoder->prefetch_mutex);
1212 if (decoder->prefetch_frame_ready && decoder->current_prefetch_image) {
1213 // Release the previous buffer (rendering is now complete)
1214 if (decoder->current_read_buffer == decoder->prefetch_image_a) {
1215 decoder->buffer_a_in_use = false;
1216 } else if (decoder->current_read_buffer == decoder->prefetch_image_b) {
1217 decoder->buffer_b_in_use = false;
1218 }
1219
1221 decoder->prefetch_frame_ready = false;
1222
1223 // Mark the new buffer as in use (prevent prefetch thread from overwriting it during rendering)
1224 if (frame == decoder->prefetch_image_a) {
1225 decoder->buffer_a_in_use = true;
1226 } else if (frame == decoder->prefetch_image_b) {
1227 decoder->buffer_b_in_use = true;
1228 }
1229
1230 decoder->current_read_buffer = frame;
1231 mutex_unlock(&decoder->prefetch_mutex);
1232
1233 // Use the prefetched frame
1234 decoder->current_image = frame;
1235 log_dev_every(5 * US_PER_SEC_INT, "Using prefetched frame");
1236 return frame;
1237 }
1238 mutex_unlock(&decoder->prefetch_mutex);
1239
1240 // No fallback synchronous decode - rely on background prefetch thread
1241 // Skipping frames when prefetch not ready allows audio timing to advance
1242 // This is critical for proper audio-video sync when prefetch is active
1244 "Prefetch frame not ready, skipping to next iteration (allow prefetch to catch up)");
1245 return NULL;
1246}
#define US_PER_SEC_INT
Definition time.h:161
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:699
image_t * current_read_buffer
Buffer main thread is currently reading/rendering.
bool buffer_b_in_use
Whether prefetch_image_b is being read by main thread.
bool buffer_a_in_use
Whether prefetch_image_a is being read by main thread.

References ffmpeg_decoder_t::buffer_a_in_use, ffmpeg_decoder_t::buffer_b_in_use, ffmpeg_decoder_t::current_image, ffmpeg_decoder_t::current_prefetch_image, ffmpeg_decoder_t::current_read_buffer, frame, log_dev_every, mutex_lock, mutex_unlock, ffmpeg_decoder_t::prefetch_frame_ready, ffmpeg_decoder_t::prefetch_image_a, ffmpeg_decoder_t::prefetch_image_b, ffmpeg_decoder_t::prefetch_mutex, US_PER_SEC_INT, and ffmpeg_decoder_t::video_stream_idx.

Referenced by media_source_read_video().

◆ ffmpeg_decoder_rewind()

asciichat_error_t ffmpeg_decoder_rewind ( ffmpeg_decoder_t *  decoder)

#include <ffmpeg_decoder.h>

Seek to beginning of media.

Parameters
decoderDecoder (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Seeks to the beginning of the media file. Used for loop implementation.

Seek process:

  1. Flush codec buffers (avcodec_flush_buffers)
  2. Seek to timestamp 0 (av_seek_frame)
  3. Clear decoder state
Note
Stdin decoders cannot seek (returns ERROR_NOT_SUPPORTED)
Some formats may not support seeking

Definition at line 1489 of file media/ffmpeg_decoder.c.

1489 {
1490 if (!decoder) {
1491 return ERROR_INVALID_PARAM;
1492 }
1493
1494 if (decoder->is_stdin) {
1495 return ERROR_NOT_SUPPORTED; // Cannot seek stdin
1496 }
1497
1498 // Flush codec buffers
1499 if (decoder->video_codec_ctx) {
1500 avcodec_flush_buffers(decoder->video_codec_ctx);
1501 }
1502 if (decoder->audio_codec_ctx) {
1503 avcodec_flush_buffers(decoder->audio_codec_ctx);
1504 }
1505
1506 // Seek to beginning
1507 if (av_seek_frame(decoder->format_ctx, -1, 0, AVSEEK_FLAG_BACKWARD) < 0) {
1508 return SET_ERRNO(ERROR_MEDIA_SEEK, "Failed to seek to beginning");
1509 }
1510
1511 decoder->eof_reached = false;
1512 decoder->video_draining = false;
1513 decoder->audio_buffer_offset = 0;
1514 decoder->last_video_pts = -1.0;
1515 decoder->last_audio_pts = -1.0;
1516 decoder->audio_samples_read = 0; // Reset sample counter to 0
1517
1518 return ASCIICHAT_OK;
1519}
@ ERROR_MEDIA_SEEK
Definition error_codes.h:73
@ ERROR_NOT_SUPPORTED
Definition error_codes.h:74
bool video_draining
Whether EOF was sent to the video decoder.

References ASCIICHAT_OK, ffmpeg_decoder_t::audio_buffer_offset, ffmpeg_decoder_t::audio_codec_ctx, ffmpeg_decoder_t::audio_samples_read, ffmpeg_decoder_t::eof_reached, ERROR_INVALID_PARAM, ERROR_MEDIA_SEEK, ERROR_NOT_SUPPORTED, ffmpeg_decoder_t::format_ctx, ffmpeg_decoder_t::is_stdin, ffmpeg_decoder_t::last_audio_pts, ffmpeg_decoder_t::last_video_pts, SET_ERRNO, ffmpeg_decoder_t::video_codec_ctx, and ffmpeg_decoder_t::video_draining.

Referenced by media_source_rewind().

◆ ffmpeg_decoder_seek_to_timestamp()

asciichat_error_t ffmpeg_decoder_seek_to_timestamp ( ffmpeg_decoder_t *  decoder,
double  timestamp_sec 
)

#include <ffmpeg_decoder.h>

Seek to specific timestamp in media.

Parameters
decoderDecoder (must not be NULL)
timestamp_secSeek target in seconds
Returns
ASCIICHAT_OK on success, error code on failure

Seeks to the specified timestamp in the media file. Used for audio/video synchronization in multi-decoder scenarios.

Seek process:

  1. Flush codec buffers (avcodec_flush_buffers)
  2. Seek to timestamp (av_seek_frame with AV_TIME_BASE conversion)
  3. Clear decoder state
Note
Stdin decoders cannot seek (returns ERROR_NOT_SUPPORTED)
Some formats may not support seeking
Seeks approximately - may land before/after exact timestamp

Definition at line 1521 of file media/ffmpeg_decoder.c.

1521 {
1522 if (!decoder) {
1523 return ERROR_INVALID_PARAM;
1524 }
1525
1526 if (decoder->is_stdin) {
1527 return ERROR_NOT_SUPPORTED; // Cannot seek stdin
1528 }
1529
1530 // Hold mutex during entire seek operation to prevent race with prefetch thread
1531 mutex_lock(&decoder->prefetch_mutex);
1532
1533 // Set flag to pause prefetch thread via condition variable
1534 decoder->seeking_in_progress = true;
1535
1536 // Convert seconds to FFmpeg time base units (AV_TIME_BASE = 1,000,000)
1537 int64_t target_ts = (int64_t)(timestamp_sec * AV_TIME_BASE);
1538
1539 // For HTTP streams, use simple keyframe seeking (faster than frame-accurate seeking)
1540 // HTTP seeking is expensive and can break stream state, so prefer speed over precision
1541 int seek_ret = av_seek_frame(decoder->format_ctx, -1, target_ts, AVSEEK_FLAG_BACKWARD);
1542 if (seek_ret < 0) {
1543 // Fallback: try without backward flag
1544 seek_ret = av_seek_frame(decoder->format_ctx, -1, target_ts, 0);
1545 }
1546
1547 if (seek_ret < 0) {
1548 decoder->seeking_in_progress = false;
1549 cond_signal(&decoder->prefetch_cond);
1550 mutex_unlock(&decoder->prefetch_mutex);
1551 return SET_ERRNO(ERROR_MEDIA_SEEK, "Failed to seek to timestamp %.2f seconds", timestamp_sec);
1552 }
1553
1554 // Flush codec buffers AFTER seeking
1555 if (decoder->video_codec_ctx) {
1556 avcodec_flush_buffers(decoder->video_codec_ctx);
1557 }
1558 if (decoder->audio_codec_ctx) {
1559 avcodec_flush_buffers(decoder->audio_codec_ctx);
1560 }
1561
1562 // Reset state
1563 decoder->eof_reached = false;
1564 decoder->video_draining = false;
1565 decoder->audio_buffer_offset = 0;
1566 // Clear any stale audio data in buffer
1567 if (decoder->audio_buffer) {
1568 memset(decoder->audio_buffer, 0, decoder->audio_buffer_size * sizeof(float));
1569 }
1570 decoder->last_video_pts = -1.0;
1571 decoder->last_audio_pts = -1.0;
1572 decoder->prefetch_frame_ready = false;
1573 // Set audio_samples_read to match the seek target so position tracking works correctly
1574 decoder->audio_samples_read = (uint64_t)(timestamp_sec * decoder->audio_sample_rate);
1575
1576 // Reset current_read_buffer and mark both buffers as not in use
1577 // After seeking, the prefetch thread may reallocate buffers, so current_read_buffer
1578 // could point to freed memory. Clear it so the next read doesn't try to release stale pointers.
1579 decoder->current_read_buffer = NULL;
1580 decoder->buffer_a_in_use = false;
1581 decoder->buffer_b_in_use = false;
1582
1583 // Resume prefetch thread
1584 decoder->seeking_in_progress = false;
1585 cond_signal(&decoder->prefetch_cond);
1586
1587 // Release mutex - prefetch thread can resume
1588 mutex_unlock(&decoder->prefetch_mutex);
1589
1590 return ASCIICHAT_OK;
1591}
int cond_signal(cond_t *cond)
Signal a condition variable (wake one waiting thread)
bool seeking_in_progress
Signal to pause prefetch thread during seek.

References ASCIICHAT_OK, ffmpeg_decoder_t::audio_buffer, ffmpeg_decoder_t::audio_buffer_offset, ffmpeg_decoder_t::audio_buffer_size, ffmpeg_decoder_t::audio_codec_ctx, ffmpeg_decoder_t::audio_sample_rate, ffmpeg_decoder_t::audio_samples_read, ffmpeg_decoder_t::buffer_a_in_use, ffmpeg_decoder_t::buffer_b_in_use, cond_signal(), ffmpeg_decoder_t::current_read_buffer, ffmpeg_decoder_t::eof_reached, ERROR_INVALID_PARAM, ERROR_MEDIA_SEEK, ERROR_NOT_SUPPORTED, ffmpeg_decoder_t::format_ctx, ffmpeg_decoder_t::is_stdin, ffmpeg_decoder_t::last_audio_pts, ffmpeg_decoder_t::last_video_pts, mutex_lock, mutex_unlock, ffmpeg_decoder_t::prefetch_cond, ffmpeg_decoder_t::prefetch_frame_ready, ffmpeg_decoder_t::prefetch_mutex, ffmpeg_decoder_t::seeking_in_progress, SET_ERRNO, ffmpeg_decoder_t::video_codec_ctx, and ffmpeg_decoder_t::video_draining.

Referenced by media_source_seek().

◆ ffmpeg_decoder_set_exit_callback()

void ffmpeg_decoder_set_exit_callback ( ffmpeg_decoder_t *  decoder,
bool(*)(void *)  should_exit_callback,
void *  user_data 
)

#include <ffmpeg_decoder.h>

Set exit signal callback for graceful shutdown during I/O.

Parameters
decoderDecoder (must not be NULL)
callbackFunction that returns true if app should exit (can be NULL)
user_dataUser data to pass to callback (can be NULL)

Sets a callback that allows the decoder's background prefetch thread to gracefully abort blocking I/O operations (like YouTube HTTP requests) when the application is shutting down (e.g., when user presses Ctrl+C).

When the callback returns true, the FFmpeg interrupt handler will abort any blocking av_read_frame() call, allowing the prefetch thread to exit cleanly rather than hanging indefinitely.

Note
Called automatically from media_source layer (no need to call manually)

Definition at line 1300 of file media/ffmpeg_decoder.c.

1301 {
1302 if (!decoder) {
1303 return;
1304 }
1305 decoder->should_exit_callback = should_exit_callback;
1306 decoder->exit_callback_user_data = user_data;
1307}
bool(* should_exit_callback)(void *user_data)
Callback to check if app should exit.
void * exit_callback_user_data
User data for exit callback.

References ffmpeg_decoder_t::exit_callback_user_data, and ffmpeg_decoder_t::should_exit_callback.

Referenced by media_source_set_exit_callback().

◆ ffmpeg_decoder_start_prefetch()

asciichat_error_t ffmpeg_decoder_start_prefetch ( ffmpeg_decoder_t *  decoder)

#include <ffmpeg_decoder.h>

Start background frame prefetching thread.

Parameters
decoderDecoder (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Starts a background thread that continuously reads and decodes video frames, storing them in an internal ringbuffer. The render loop then pulls pre-decoded frames from this buffer without blocking on network I/O.

Purpose: Solves YouTube HTTP streaming performance issues by decoupling the blocking av_read_frame() calls from the render loop. Without this, frames take 93ms to arrive instead of the expected 41.7ms at 24 FPS.

Note
Should be called after decoder creation for FILE/HTTP sources
Automatically called when media source is created
Thread is stopped in ffmpeg_decoder_destroy()

Start background frame prefetching thread.

Starts a background thread that continuously reads and decodes video frames, storing them in double-buffer structures. The render loop pulls frames from these buffers. This prevents the render thread from blocking on YouTube HTTP requests.

Definition at line 1255 of file media/ffmpeg_decoder.c.

1255 {
1256 if (!decoder || decoder->video_stream_idx < 0) {
1257 return ERROR_INVALID_PARAM;
1258 }
1259
1260 if (!decoder->prefetch_image_a || !decoder->prefetch_image_b) {
1261 return ERROR_INVALID_PARAM;
1262 }
1263
1264 // Already running
1265 if (decoder->prefetch_thread_running) {
1266 return ASCIICHAT_OK;
1267 }
1268
1269 // Reset stop flag and create thread
1270 decoder->prefetch_should_stop = false;
1271
1272 int thread_err = asciichat_thread_create(&decoder->prefetch_thread, "ffmpeg_prefetch",
1273 ffmpeg_decoder_prefetch_thread_func, decoder);
1274 if (thread_err != 0) {
1275 return SET_ERRNO(ERROR_THREAD, "Failed to create video prefetch thread");
1276 }
1277
1278 decoder->prefetch_thread_running = true;
1279 return ASCIICHAT_OK;
1280}
@ ERROR_THREAD
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)

References ASCIICHAT_OK, asciichat_thread_create, ERROR_INVALID_PARAM, ERROR_THREAD, ffmpeg_decoder_t::prefetch_image_a, ffmpeg_decoder_t::prefetch_image_b, ffmpeg_decoder_t::prefetch_should_stop, ffmpeg_decoder_t::prefetch_thread, ffmpeg_decoder_t::prefetch_thread_running, SET_ERRNO, and ffmpeg_decoder_t::video_stream_idx.

Referenced by media_source_create().

◆ ffmpeg_decoder_stop_prefetch()

void ffmpeg_decoder_stop_prefetch ( ffmpeg_decoder_t *  decoder)

#include <ffmpeg_decoder.h>

Stop background frame prefetching thread.

Parameters
decoderDecoder (must not be NULL)

Stops the background prefetch thread. Automatically called by ffmpeg_decoder_destroy(). Safe to call even if thread isn't running.

Stop background frame prefetching thread.

Definition at line 1285 of file media/ffmpeg_decoder.c.

1285 {
1286 if (!decoder || !decoder->prefetch_thread_running) {
1287 return;
1288 }
1289
1290 decoder->prefetch_should_stop = true;
1291 // Wait up to 2 seconds for thread to stop
1292 // The interrupt callback should cause av_read_frame to abort quickly
1294
1295 // Mark thread as stopped regardless of join result
1296 // (if join succeeded: cleanup was done; if timeout: will be cleaned up on restart)
1297 decoder->prefetch_thread_running = false;
1298}
#define NS_PER_MS_INT
Definition time.h:156
int asciichat_thread_join_timeout(asciichat_thread_t *thread, void **retval, uint64_t timeout_ns)
Wait for a thread to complete with timeout.

References asciichat_thread_join_timeout(), NS_PER_MS_INT, ffmpeg_decoder_t::prefetch_should_stop, ffmpeg_decoder_t::prefetch_thread, and ffmpeg_decoder_t::prefetch_thread_running.

◆ media_source_at_end()

bool media_source_at_end ( media_source_t *  source)

#include <source.h>

Check if media source reached end of stream.

Parameters
sourceMedia source (must not be NULL)
Returns
true if end reached, false otherwise

Determines if the media source has reached end of stream. This distinguishes between read errors and legitimate EOF conditions.

Return values:

  • true: End of stream reached (no more data)
  • false: More data available or error occurred
Note
WEBCAM/TEST sources never reach end (always return false)
FILE/STDIN sources return true when all packets consumed
If loop is enabled, this returns false (EOF triggers rewind)

Definition at line 772 of file source.c.

772 {
773 if (!source) {
774 return true;
775 }
776
777 switch (source->type) {
780 return false; // Webcam never ends
781
784 if (!source->video_decoder) {
785 return true;
786 }
787 // If loop is enabled, we never truly reach end
788 if (source->loop_enabled && source->type == MEDIA_SOURCE_FILE) {
789 return false;
790 }
791 if (source->audio_only)
792 return source->audio_decoder && ffmpeg_decoder_at_end(source->audio_decoder);
793 return ffmpeg_decoder_at_end(source->video_decoder);
794
795 default:
796 return true;
797 }
798}
bool ffmpeg_decoder_at_end(ffmpeg_decoder_t *decoder)
Check if decoder reached end of stream.
bool audio_only
File audio uses the selected webcam for video.
Definition source.c:37
media_source_type_t type
Type of media source (webcam, file, stdin, test)
Definition source.c:35
ffmpeg_decoder_t * audio_decoder
Audio decoder (separate or shared with video)
Definition source.c:46
ffmpeg_decoder_t * video_decoder
Video decoder (separate or shared with audio)
Definition source.c:45
bool loop_enabled
Whether to loop playback (for files)
Definition source.c:36

References media_source_t::audio_decoder, media_source_t::audio_only, ffmpeg_decoder_at_end(), media_source_t::loop_enabled, MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, media_source_t::type, and media_source_t::video_decoder.

Referenced by media_source_read_video(), and session_capture_at_end().

◆ media_source_create()

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

#include <source.h>

Create a new media source.

Parameters
typeMedia source type
pathFile path (for FILE type), device index string (for WEBCAM), or NULL
Returns
Pointer to media source, or NULL on failure

Creates and initializes a media source of the specified type.

Path parameter usage:

  • MEDIA_SOURCE_FILE: File path (e.g., "video.mp4")
  • MEDIA_SOURCE_STDIN: Use "-" or NULL
  • MEDIA_SOURCE_WEBCAM: Device index as string (e.g., "0") or NULL for default
  • MEDIA_SOURCE_TEST: Ignored (use NULL)
Note
For WEBCAM type, delegates to webcam_init_context()
For FILE/STDIN types, uses FFmpeg decoder
Call media_source_destroy() to cleanup

Definition at line 187 of file source.c.

187 {
189 if (!source) {
190 SET_ERRNO(ERROR_MEMORY, "Failed to allocate media source");
191 return NULL;
192 }
193
194 memset(source, 0, sizeof(*source));
195 source->type = type;
196 source->loop_enabled = false;
197 source->is_paused = false;
198
199 // Initialize mutex for protecting shared decoder access (for YouTube URLs)
200 if (mutex_init(&source->decoder_mutex, "media_decoder") != 0) {
201 SET_ERRNO(ERROR_MEMORY, "Failed to initialize mutex");
202 SAFE_FREE(source);
203 return NULL;
204 }
205
206 // Initialize mutex for protecting pause state (accessed from keyboard and video threads)
207 if (mutex_init(&source->pause_mutex, "media_pause") != 0) {
208 SET_ERRNO(ERROR_MEMORY, "Failed to initialize pause mutex");
210 SAFE_FREE(source);
211 return NULL;
212 }
213
214 // Initialize mutex for protecting decoder access during seeks (prevents race conditions)
215 if (mutex_init(&source->seek_access_mutex, "media_seek") != 0) {
216 SET_ERRNO(ERROR_MEMORY, "Failed to initialize seek access mutex");
218 mutex_destroy(&source->pause_mutex);
219 SAFE_FREE(source);
220 return NULL;
221 }
222
223 switch (type) {
224 case MEDIA_SOURCE_WEBCAM: {
225 // Parse webcam index from path (if provided)
226 unsigned short int index = 0;
227 if (path) {
228 char *endptr;
229 errno = 0;
230 long parsed = strtol(path, &endptr, 10);
231 if (*endptr == '\0' && errno == 0 && parsed >= 0 && parsed <= USHRT_MAX) {
232 index = (unsigned short int)parsed;
233 } else if (*endptr != '\0' || errno != 0) {
234 log_error("media_source_create: Invalid webcam index: %s", path);
235 }
236 }
237
238 source->webcam_index = index;
239 asciichat_error_t webcam_error = webcam_init_context(&source->webcam_ctx, index);
240 if (webcam_error != ASCIICHAT_OK) {
241 // Webcam init failed - log and cleanup
242 log_error("Failed to initialize webcam device %u (error code: %d)", index, webcam_error);
243 // Destroy mutexes before freeing source
245 mutex_destroy(&source->pause_mutex);
247 SAFE_FREE(source);
248
249 // Explicitly re-set errno to preserve the specific error code for the caller
250 // (log_error or other calls may have cleared the thread-local errno)
251 if (webcam_error == ERROR_WEBCAM_IN_USE) {
252 SET_ERRNO(ERROR_WEBCAM_IN_USE, "Webcam device %u is in use", index);
253 } else {
254 SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam device %u", index);
255 }
256 return NULL;
257 }
258
259 log_debug("Media source: Webcam device %u", index);
260 break;
261 }
262
263 case MEDIA_SOURCE_FILE: {
264 if (!path || path[0] == '\0') {
265 SET_ERRNO(ERROR_INVALID_PARAM, "File path is required for FILE source");
266 // Destroy mutexes before freeing source
268 mutex_destroy(&source->pause_mutex);
270 SAFE_FREE(source);
271 return NULL;
272 }
273
274 // Resolve URL using smart FFmpeg/yt-dlp routing
275 const char *effective_path = path;
276 char resolved_url[BUFFER_SIZE_XLARGE] = {0};
277 const char *yt_dlp_options = GET_OPTION(yt_dlp_options);
278
279 log_debug("Resolving URL: %s", path);
280 asciichat_error_t resolve_err = media_source_resolve_url(path, yt_dlp_options, resolved_url, sizeof(resolved_url));
281 if (resolve_err != ASCIICHAT_OK) {
282 log_debug("Failed to resolve URL (error: %d)", resolve_err);
283 // Destroy mutexes before freeing source
285 mutex_destroy(&source->pause_mutex);
287 SAFE_FREE(source);
288 return NULL;
289 }
290 effective_path = resolved_url;
291 log_debug("Using resolved URL for playback");
292
293 // Store original URL for potential re-extraction if needed
295 if (!source->original_youtube_url) {
296 log_warn("Failed to cache original URL");
297 }
298
299 // Cache file path for potential reopen on loop
300 source->file_path = platform_strdup(effective_path);
301 if (!source->file_path) {
302 SET_ERRNO(ERROR_MEMORY, "Failed to duplicate file path");
304 // Destroy mutexes before freeing source
306 mutex_destroy(&source->pause_mutex);
308 SAFE_FREE(source);
309 return NULL;
310 }
311
312 // Always use separate decoders for video and audio
313 // This allows independent read rates and avoids lock contention
314 source->video_decoder = ffmpeg_decoder_create(effective_path);
315 if (!source->video_decoder) {
316 log_error("Failed to open media file for video: %s", effective_path);
317 SAFE_FREE(source->file_path);
319 // Destroy mutexes before freeing source
321 mutex_destroy(&source->pause_mutex);
323 SAFE_FREE(source);
324 return NULL;
325 }
326
327 // Start prefetch only when a video stream exists.
328 // This thread continuously reads frames into a buffer so the render loop never blocks
331 : ASCIICHAT_OK;
332 if (prefetch_err != ASCIICHAT_OK) {
333 log_error("Failed to start video prefetch thread: %s", asciichat_error_string(prefetch_err));
334 // Don't fail on prefetch error - continue with frame skipping as fallback
335 }
336
337 source->audio_decoder = ffmpeg_decoder_create(effective_path);
338 if (!source->audio_decoder) {
339 log_error("Failed to open media file for audio: %s", effective_path);
341 source->video_decoder = NULL;
342 SAFE_FREE(source->file_path);
344 // Destroy mutexes before freeing source
346 mutex_destroy(&source->pause_mutex);
348 SAFE_FREE(source);
349 return NULL;
350 }
351 source->is_shared_decoder = false;
352
353 log_debug("Media source: URL resolved to stream (separate video/audio decoders)");
354 break;
355 }
356
357 case MEDIA_SOURCE_STDIN: {
358 // Create separate decoders for video and audio from stdin
360 if (!source->video_decoder) {
361 log_error("Failed to open stdin for video input");
362 // Destroy mutexes before freeing source
364 mutex_destroy(&source->pause_mutex);
366 SAFE_FREE(source);
367 return NULL;
368 }
369
370 // Start prefetch thread for stdin video frames
373 : ASCIICHAT_OK;
374 if (prefetch_err != ASCIICHAT_OK) {
375 log_error("Failed to start stdin video prefetch thread: %s", asciichat_error_string(prefetch_err));
376 // Don't fail on prefetch error - continue with frame skipping as fallback
377 }
378
380 if (!source->audio_decoder) {
381 log_error("Failed to open stdin for audio input");
383 source->video_decoder = NULL;
384 // Destroy mutexes before freeing source
386 mutex_destroy(&source->pause_mutex);
388 SAFE_FREE(source);
389 return NULL;
390 }
391
392 log_debug("Media source: stdin (separate video/audio decoders)");
393 break;
394 }
395
396 case MEDIA_SOURCE_TEST: {
397 // Test pattern state is encapsulated in media_source_t
398 source->webcam_index = 0;
399 source->webcam_ctx = NULL; // No context needed for test pattern
400 source->test_pattern_frame = NULL; // Allocated lazily on first read
401 source->test_frame_counter = 0;
402
403 log_debug("Media source: Test pattern");
404 break;
405 }
406
407 default:
408 SET_ERRNO(ERROR_INVALID_PARAM, "Unknown media source type: %d", type);
409 // Destroy mutexes before freeing source
411 mutex_destroy(&source->pause_mutex);
413 SAFE_FREE(source);
414 return NULL;
415 }
416
417 if ((type == MEDIA_SOURCE_FILE || type == MEDIA_SOURCE_STDIN) && !ffmpeg_decoder_has_video(source->video_decoder) &&
419 source->audio_only = true;
420 }
421
422 /* Register media source with named registry */
423 char media_type_str[64];
424 switch (type) {
426 snprintf(media_type_str, sizeof(media_type_str), "media_source:webcam");
427 break;
429 snprintf(media_type_str, sizeof(media_type_str), "media_source:file");
430 break;
432 snprintf(media_type_str, sizeof(media_type_str), "media_source:test");
433 break;
434 default:
435 snprintf(media_type_str, sizeof(media_type_str), "media_source:unknown");
436 break;
437 }
438 NAMED_REGISTER(source, media_type_str, "media_source_t", "0x%tx", NULL);
439
440 /* Register media source's sync primitives with hierarchical naming */
441 NAMED_REGISTER_MUTEX(&source->decoder_mutex, "decoder_mutex", (uintptr_t)(const void *)(source));
442 NAMED_REGISTER_MUTEX(&source->pause_mutex, "pause_mutex", (uintptr_t)(const void *)(source));
443 NAMED_REGISTER_MUTEX(&source->seek_access_mutex, "seek_access_mutex", (uintptr_t)(const void *)(source));
444
445 return source;
446}
#define BUFFER_SIZE_XLARGE
Extra large buffer size (2048 bytes)
#define NAMED_REGISTER(ptr, name, type, fmt, parent_ptr)
Register any pointer with base name, type, format spec, and location (auto-suffix)
#define NAMED_REGISTER_MUTEX(mutex, name, parent_ptr)
Register a mutex with automatic format specifier.
@ ERROR_WEBCAM_IN_USE
Definition error_codes.h:65
@ ERROR_WEBCAM
Definition error_codes.h:64
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
bool ffmpeg_decoder_has_video(ffmpeg_decoder_t *decoder)
Check if decoder has video stream.
bool ffmpeg_decoder_has_audio(ffmpeg_decoder_t *decoder)
Check if decoder has audio stream.
asciichat_error_t ffmpeg_decoder_start_prefetch(ffmpeg_decoder_t *decoder)
Start the background frame prefetching thread.
ffmpeg_decoder_t * ffmpeg_decoder_create(const char *path)
Create FFmpeg decoder from file path.
ffmpeg_decoder_t * ffmpeg_decoder_create_stdin(void)
Create FFmpeg decoder from stdin.
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
int errno
asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index)
Initialize webcam context for advanced operations.
webcam_context_t * webcam_ctx
Webcam context (NULL for non-webcam types)
Definition source.c:41
bool is_shared_decoder
True if both streams share same decoder (YouTube URLs)
Definition source.c:47
unsigned int test_frame_counter
Animation phase counter for test pattern.
Definition source.c:63
char * file_path
File path (for FILE type)
Definition source.c:55
char * original_youtube_url
Original YouTube URL for re-extraction.
Definition source.c:56
image_t * test_pattern_frame
Reusable 320x240 frame buffer for test pattern.
Definition source.c:62
mutex_t seek_access_mutex
Protects decoder access during seeks.
Definition source.c:51
mutex_t decoder_mutex
Protects shared decoder access (YouTube URLs)
Definition source.c:50
mutex_t pause_mutex
Protects pause state.
Definition source.c:52
unsigned short int webcam_index
Webcam device index.
Definition source.c:42
bool is_paused
Whether playback is paused.
Definition source.c:38

References ASCIICHAT_OK, media_source_t::audio_decoder, media_source_t::audio_only, BUFFER_SIZE_XLARGE, media_source_t::decoder_mutex, errno, ERROR_INVALID_PARAM, ERROR_MEMORY, ERROR_WEBCAM, ERROR_WEBCAM_IN_USE, ffmpeg_decoder_create(), ffmpeg_decoder_create_stdin(), ffmpeg_decoder_destroy(), ffmpeg_decoder_has_audio(), ffmpeg_decoder_has_video(), ffmpeg_decoder_start_prefetch(), media_source_t::file_path, GET_OPTION, media_source_t::is_paused, media_source_t::is_shared_decoder, log_debug, log_error, log_warn, media_source_t::loop_enabled, MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, mutex_destroy(), mutex_init(), NAMED_REGISTER, NAMED_REGISTER_MUTEX, media_source_t::original_youtube_url, media_source_t::pause_mutex, platform_strdup(), SAFE_FREE, SAFE_MALLOC, media_source_t::seek_access_mutex, SET_ERRNO, media_source_t::test_frame_counter, media_source_t::test_pattern_frame, media_source_t::type, media_source_t::video_decoder, media_source_t::webcam_ctx, media_source_t::webcam_index, and webcam_init_context().

Referenced by session_capture_create(), and session_client_like_run().

◆ media_source_destroy()

void media_source_destroy ( media_source_t *  source)

#include <source.h>

Destroy media source and free resources.

Parameters
sourceMedia source to destroy (can be NULL)

Cleans up media source and releases all resources. Safe to call with NULL.

Note
For WEBCAM type, calls webcam_cleanup_context()
For FILE/STDIN types, closes FFmpeg decoder

Definition at line 448 of file source.c.

448 {
449 if (!source) {
450 return;
451 }
452
453 // Cleanup based on type
454 if (source->webcam_ctx) {
456 source->webcam_ctx = NULL;
457 }
458
459 // For YouTube (shared decoder), only destroy once
460 // For local files (separate decoders), destroy both
461 if (source->video_decoder) {
463 source->video_decoder = NULL;
464 }
465
466 if (source->audio_decoder) {
467 // Don't double-free if audio and video share the same decoder
468 if (!source->is_shared_decoder) {
470 }
471 source->audio_decoder = NULL;
472 }
473
474 SAFE_FREE(source->file_path);
476
477 // Clean up test pattern frame
478 if (source->test_pattern_frame) {
480 source->test_pattern_frame = NULL;
481 }
482
483 // Destroy mutexes
485 mutex_destroy(&source->pause_mutex);
487
488 SAFE_FREE(source);
489}
void webcam_cleanup_context(webcam_context_t *ctx)
Clean up webcam context and release resources.

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

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

◆ media_source_get_duration()

double media_source_get_duration ( media_source_t *  source)

#include <source.h>

Get media duration in seconds.

Parameters
sourceMedia source (must not be NULL)
Returns
Duration in seconds, or -1.0 if unknown/infinite

Returns the total duration of the media. Useful for progress display.

Return values:

  • Positive value: Duration in seconds (e.g., 120.5 for 2 minutes)
  • -1.0: Duration unknown (stdin, live streams) or infinite (webcam)
Note
WEBCAM/TEST sources return -1.0 (infinite)
FILE sources return actual duration from container
STDIN sources return -1.0 (cannot determine duration)

Definition at line 934 of file source.c.

934 {
935 if (!source) {
936 return -1.0;
937 }
938
939 switch (source->type) {
942 return -1.0; // Infinite
943
946 if (!source->video_decoder) {
947 return -1.0;
948 }
950
951 default:
952 return -1.0;
953 }
954}
double ffmpeg_decoder_get_duration(ffmpeg_decoder_t *decoder)
Get media duration in seconds.

References ffmpeg_decoder_get_duration(), MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, media_source_t::type, and media_source_t::video_decoder.

Referenced by session_handle_keyboard_input().

◆ media_source_get_position()

double media_source_get_position ( media_source_t *  source)

#include <source.h>

Get current playback position in seconds.

Parameters
sourceMedia source (must not be NULL)
Returns
Current position in seconds, or -1.0 if unknown

Returns the current playback position. Useful for progress display.

Return values:

  • Positive value: Position in seconds (e.g., 45.2)
  • -1.0: Position unknown or not applicable
Note
WEBCAM/TEST sources return -1.0 (no position concept)
FILE sources return position based on last decoded frame PTS

Definition at line 956 of file source.c.

956 {
957 if (!source) {
958 return -1.0;
959 }
960
961 switch (source->type) {
964 return -1.0; // No position concept
965
968 if (!source->video_decoder) {
969 return -1.0;
970 }
971 return ffmpeg_decoder_get_position(source->audio_only ? source->audio_decoder : source->video_decoder);
972
973 default:
974 return -1.0;
975 }
976}
double ffmpeg_decoder_get_position(ffmpeg_decoder_t *decoder)
Get current playback position in seconds.

References media_source_t::audio_decoder, media_source_t::audio_only, ffmpeg_decoder_get_position(), MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, media_source_t::type, and media_source_t::video_decoder.

Referenced by session_handle_keyboard_input().

◆ media_source_get_type()

media_source_type_t media_source_get_type ( media_source_t *  source)

#include <source.h>

Get media source type.

Parameters
sourceMedia source (must not be NULL)
Returns
Media source type

Returns the type of the media source.

Definition at line 930 of file source.c.

930 {
931 return source ? source->type : MEDIA_SOURCE_TEST;
932}

References MEDIA_SOURCE_TEST, and media_source_t::type.

Referenced by session_capture_sleep_for_fps(), session_handle_keyboard_input(), and session_pipeline_create().

◆ media_source_get_video_fps()

double media_source_get_video_fps ( media_source_t *  source)

#include <source.h>

Get video frame rate in frames per second.

Parameters
sourceMedia source (must not be NULL)
Returns
Frame rate in FPS, or 0.0 if unknown

Returns the native frame rate of the video stream.

Return values:

  • Positive value: Frame rate in FPS (e.g., 30.0, 60.0)
  • 0.0: Frame rate unknown or not applicable
Note
WEBCAM sources return 0.0 (variable rate)
TEST sources return 0.0
FILE/STDIN sources return the video stream's fps from FFmpeg

Definition at line 978 of file source.c.

978 {
979 if (!source) {
980 return 0.0;
981 }
982
983 switch (source->type) {
986 return 0.0; // Variable rate, no fixed FPS
987
990 if (!source->video_decoder) {
991 return 0.0;
992 }
994
995 default:
996 return 0.0;
997 }
998}
double ffmpeg_decoder_get_video_fps(ffmpeg_decoder_t *decoder)
Get video frame rate.

References ffmpeg_decoder_get_video_fps(), MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, media_source_t::type, and media_source_t::video_decoder.

Referenced by session_client_like_run().

◆ media_source_has_audio()

bool media_source_has_audio ( media_source_t *  source)

#include <source.h>

Check if media source has audio stream.

Parameters
sourceMedia source (must not be NULL)
Returns
true if source has audio, false otherwise

Determines if the media source provides audio samples.

Note
WEBCAM and TEST sources return false (no audio)
FILE/STDIN sources return true if audio stream detected

Definition at line 737 of file source.c.

737 {
738 if (!source) {
739 return false;
740 }
741
742 switch (source->type) {
745 return false; // Webcam/test don't have audio
746
749 return source->audio_decoder && ffmpeg_decoder_has_audio(source->audio_decoder);
750
751 default:
752 return false;
753 }
754}

References media_source_t::audio_decoder, ffmpeg_decoder_has_audio(), MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, and media_source_t::type.

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

◆ media_source_has_video()

bool media_source_has_video ( media_source_t *  source)

#include <source.h>

Check if media source has video stream.

Parameters
sourceMedia source (must not be NULL)
Returns
true if source has video, false otherwise

Determines if the media source provides video frames.

Note
WEBCAM and TEST sources always return true
FILE/STDIN sources return true if video stream detected

Definition at line 629 of file source.c.

629 {
630 if (!source) {
631 return false;
632 }
633
634 switch (source->type) {
637 return true; // Webcam/test always has video
638
641 return source->audio_only || (source->video_decoder && ffmpeg_decoder_has_video(source->video_decoder));
642
643 default:
644 return false;
645 }
646}

References media_source_t::audio_only, ffmpeg_decoder_has_video(), MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, media_source_t::type, and media_source_t::video_decoder.

◆ media_source_is_paused()

bool media_source_is_paused ( media_source_t *  source)

#include <source.h>

Check if media source is paused.

Parameters
sourceMedia source (must not be NULL)
Returns
true if paused, false if playing

Determines if the media source is currently paused.

Definition at line 1022 of file source.c.

1022 {
1023 if (!source) {
1024 return false;
1025 }
1026 mutex_lock(&source->pause_mutex);
1027 bool paused = source->is_paused;
1028 mutex_unlock(&source->pause_mutex);
1029 return paused;
1030}

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

Referenced by session_handle_keyboard_input().

◆ media_source_pause()

void media_source_pause ( media_source_t *  source)

#include <source.h>

Pause media playback.

Parameters
sourceMedia source (must not be NULL)

Pauses the media source. When paused, read_video and read_audio return no new data (NULL/silence) while maintaining the current playback position. Resume with media_source_resume() to continue from the paused position.

Behavior:

Note
All source types support pause (has no effect for WEBCAM/TEST)
Pausing is instantaneous (next read will return no data)

Definition at line 1004 of file source.c.

1004 {
1005 if (!source) {
1006 return;
1007 }
1008 mutex_lock(&source->pause_mutex);
1009 source->is_paused = true;
1010 mutex_unlock(&source->pause_mutex);
1011}

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

Referenced by session_capture_read_frame().

◆ media_source_read_audio()

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

#include <source.h>

Read audio samples from media source.

Parameters
sourceMedia source (must not be NULL)
bufferOutput buffer for audio samples (must not be NULL)
num_samplesNumber of samples to read
Returns
Number of samples actually read (may be less than requested)

Reads audio samples from the media source. Samples are in 32-bit float format, mono channel, 48kHz sample rate (compatible with Opus encoding).

Audio format:

  • Format: 32-bit float
  • Channels: Mono (1 channel)
  • Sample rate: 48000 Hz
  • Range: -1.0 to 1.0

Return value:

  • Returns actual number of samples read (0 to num_samples)
  • Returns 0 on error or end of stream
  • Check media_source_at_end() to distinguish error from EOF
Note
WEBCAM type does NOT provide audio (returns 0)
FILE/STDIN types decode audio via FFmpeg if stream exists
Audio is resampled to 48kHz mono automatically

Definition at line 652 of file source.c.

652 {
653 if (!source || !buffer || num_samples == 0) {
654 return 0;
655 }
656
657 static uint64_t call_count = 0;
658 call_count++;
659 if (call_count <= 5 || call_count % 1000 == 0) {
660 log_info("media_source_read_audio #%lu: source_type=%d num_samples=%zu", call_count, source->type, num_samples);
661 }
662
663 // Check pause state (thread-safe)
664 mutex_lock(&source->pause_mutex);
665 bool is_paused = source->is_paused;
666 mutex_unlock(&source->pause_mutex);
667
668 // Return silence immediately if paused (maintaining position)
669 if (is_paused) {
670 memset(buffer, 0, num_samples * sizeof(float));
671 return num_samples;
672 }
673
674 switch (source->type) {
677 // Webcam/test pattern don't provide audio
678 return 0;
679
681 case MEDIA_SOURCE_STDIN: {
682 if (!source->audio_decoder) {
683 log_warn("media_source_read_audio: audio_decoder is NULL!");
684 return 0;
685 }
686
687 // Lock seek_access_mutex to prevent audio callback from reading during seek
689
690 // Lock shared decoder if YouTube URL (protect against concurrent video thread access)
691 if (source->is_shared_decoder) {
692 mutex_lock(&source->decoder_mutex);
693 }
694
695 double audio_pos_before_read = ffmpeg_decoder_get_position(source->audio_decoder);
696 size_t samples_read = ffmpeg_decoder_read_audio_samples(source->audio_decoder, buffer, num_samples);
697 double audio_pos_after_read = ffmpeg_decoder_get_position(source->audio_decoder);
698
699 static double last_audio_pos = 0;
700 if (audio_pos_after_read >= 0 && last_audio_pos >= 0 && audio_pos_after_read < last_audio_pos) {
701 log_warn("AUDIO POSITION WENT BACKWARD: %.2f → %.2f (LOOPING!)", last_audio_pos, audio_pos_after_read);
702 }
703 if (audio_pos_after_read >= 0) {
704 last_audio_pos = audio_pos_after_read;
705 }
706
707 log_info_every(100 * US_PER_MS_INT, "Audio: read %zu samples, pos %.2f → %.2f", samples_read,
708 audio_pos_before_read, audio_pos_after_read);
709
710 // Handle EOF with loop
711 if (samples_read == 0 && ffmpeg_decoder_at_end(source->audio_decoder)) {
712 if (source->loop_enabled && source->type == MEDIA_SOURCE_FILE) {
713 log_debug("End of file reached (audio), rewinding for loop");
714 if (media_source_rewind(source) == ASCIICHAT_OK) {
715 // Try reading again after rewind
716 samples_read = ffmpeg_decoder_read_audio_samples(source->audio_decoder, buffer, num_samples);
717 }
718 }
719 }
720
721 // Unlock shared decoder
722 if (source->is_shared_decoder) {
723 mutex_unlock(&source->decoder_mutex);
724 }
725
726 // Release seek_access_mutex
728
729 return samples_read;
730 }
731
732 default:
733 return 0;
734 }
735}
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
asciichat_error_t media_source_rewind(media_source_t *source)
Rewind media source to beginning.
Definition source.c:800
size_t ffmpeg_decoder_read_audio_samples(ffmpeg_decoder_t *decoder, float *buffer, size_t num_samples)
Decode audio samples.

References ASCIICHAT_OK, media_source_t::audio_decoder, media_source_t::decoder_mutex, ffmpeg_decoder_at_end(), ffmpeg_decoder_get_position(), ffmpeg_decoder_read_audio_samples(), media_source_t::is_paused, media_source_t::is_shared_decoder, log_debug, log_info, log_info_every, log_warn, media_source_t::loop_enabled, MEDIA_SOURCE_FILE, media_source_rewind(), MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, mutex_lock, mutex_unlock, media_source_t::pause_mutex, media_source_t::seek_access_mutex, media_source_t::type, and US_PER_MS_INT.

Referenced by session_capture_read_audio().

◆ media_source_read_video()

image_t * media_source_read_video ( media_source_t *  source)

#include <source.h>

Read next video frame from media source.

Parameters
sourceMedia source (must not be NULL)
Returns
Pointer to image_t frame, or NULL on error/end

Reads the next video frame from the media source. Returns an image_t structure in RGB format, compatible with existing ascii-chat pipeline.

Return values:

  • Non-NULL: Valid frame pointer (do NOT free - internal buffer)
  • NULL: Error or end of stream (check media_source_at_end())

Frame ownership:

  • Returned frame is owned by media source
  • Do NOT free the returned frame
  • Frame is valid until next call or source destruction
Note
For WEBCAM type, calls webcam_read_context()
For FILE/STDIN types, decodes next video frame via FFmpeg
Frame timing is handled by caller (capture thread)

Definition at line 523 of file source.c.

523 {
524 if (!source) {
525 return NULL;
526 }
527
528 // Check pause state (thread-safe)
529 mutex_lock(&source->pause_mutex);
530 bool is_paused = source->is_paused;
531 mutex_unlock(&source->pause_mutex);
532
533 // Return NULL immediately if paused (maintaining position)
534 if (is_paused) {
535 return NULL;
536 }
537
538 if (source->audio_only && media_source_at_end(source))
539 return NULL;
540 if (source->audio_only && GET_OPTION(test_pattern))
541 return read_test_pattern(source);
542 if (source->audio_only && !source->webcam_ctx) {
544 return NULL;
545 }
546 if (source->webcam_ctx)
547 return webcam_read_context(source->webcam_ctx);
548
549 switch (source->type) {
551 // Read from webcam
552 if (source->webcam_ctx) {
553 return webcam_read_context(source->webcam_ctx);
554 }
555 return NULL;
556
557 case MEDIA_SOURCE_TEST: {
558 return read_test_pattern(source);
559 }
560
562 case MEDIA_SOURCE_STDIN: {
563 if (!source->video_decoder) {
564 return NULL;
565 }
566
567 // Lock shared decoder if YouTube URL (protect against concurrent audio thread access)
568 if (source->is_shared_decoder) {
569 mutex_lock(&source->decoder_mutex);
570 }
571
572 uint64_t frame_read_start_ns = time_get_ns();
574 uint64_t frame_read_ns = time_elapsed_ns(frame_read_start_ns, time_get_ns());
575
576 // Track frame reading statistics for FPS diagnosis
577 static uint64_t total_attempts = 0;
578 static uint64_t successful_frames = 0;
579 static uint64_t null_frame_count = 0;
580 static uint64_t total_read_time_ns = 0;
581 static uint64_t max_read_time_ns = 0;
582
583 total_attempts++;
584 if (frame) {
585 successful_frames++;
586 total_read_time_ns += frame_read_ns;
587 if (frame_read_ns > max_read_time_ns) {
588 max_read_time_ns = frame_read_ns;
589 }
590
591 // Log statistics every 30 successful frames
592 if (successful_frames % 30 == 0) {
593 double avg_read_ms = (double)total_read_time_ns / (double)successful_frames / NS_PER_MS;
594 double max_read_ms = (double)max_read_time_ns / NS_PER_MS;
595 double null_rate = (double)null_frame_count * 100.0 / (double)total_attempts;
597 "FRAME_STATS[%lu]: avg_read=%.2f ms, max_read=%.2f ms, null_rate=%.1f%% "
598 "(%lu null/%lu attempts)",
599 successful_frames, avg_read_ms, max_read_ms, null_rate, null_frame_count, total_attempts);
600 }
601 } else {
602 null_frame_count++;
603 }
604
605 // Handle EOF with loop
606 if (!frame && ffmpeg_decoder_at_end(source->video_decoder)) {
607 if (source->loop_enabled && source->type == MEDIA_SOURCE_FILE) {
608 log_debug("End of file reached, rewinding for loop");
609 if (media_source_rewind(source) == ASCIICHAT_OK) {
610 // Try reading again after rewind
612 }
613 }
614 }
615
616 // Unlock shared decoder
617 if (source->is_shared_decoder) {
618 mutex_unlock(&source->decoder_mutex);
619 }
620
621 return frame;
622 }
623
624 default:
625 return NULL;
626 }
627}
image_t * ffmpeg_decoder_read_video_frame(ffmpeg_decoder_t *decoder)
Decode next video frame.
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
#define NS_PER_MS
Definition time.h:147
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition util/time.c:150
image_t * webcam_read_context(webcam_context_t *ctx)
Capture a frame from webcam context.
asciichat_error_t media_source_start_video(media_source_t *source)
Definition source.c:506

References ASCIICHAT_OK, media_source_t::audio_only, media_source_t::decoder_mutex, ffmpeg_decoder_at_end(), ffmpeg_decoder_read_video_frame(), frame, GET_OPTION, media_source_t::is_paused, media_source_t::is_shared_decoder, log_debug, log_info_every, media_source_t::loop_enabled, media_source_at_end(), MEDIA_SOURCE_FILE, media_source_rewind(), media_source_start_video(), MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, mutex_lock, mutex_unlock, NS_PER_MS, media_source_t::pause_mutex, time_elapsed_ns(), time_get_ns(), media_source_t::type, US_PER_SEC_INT, media_source_t::video_decoder, media_source_t::webcam_ctx, and webcam_read_context().

Referenced by session_capture_read_frame().

◆ media_source_resume()

void media_source_resume ( media_source_t *  source)

#include <source.h>

Resume media playback after pause.

Parameters
sourceMedia source (must not be NULL)

Resumes playback from where it was paused. Continues reading frames and audio samples from the current position.

Note
Safe to call if not paused (no-op)
Resume is instantaneous (next read will return new data)

Definition at line 1013 of file source.c.

1013 {
1014 if (!source) {
1015 return;
1016 }
1017 mutex_lock(&source->pause_mutex);
1018 source->is_paused = false;
1019 mutex_unlock(&source->pause_mutex);
1020}

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

◆ media_source_rewind()

asciichat_error_t media_source_rewind ( media_source_t *  source)

#include <source.h>

Rewind media source to beginning.

Parameters
sourceMedia source (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Seeks to the beginning of the media source. Used for loop implementation.

Behavior:

  • FILE sources: Seek to timestamp 0 (if seekable)
  • STDIN sources: ERROR_NOT_SUPPORTED (cannot seek stdin)
  • WEBCAM/TEST sources: No-op (always returns OK)
Note
Called automatically by read functions if loop enabled
Can be called manually to restart playback

Definition at line 800 of file source.c.

800 {
801 if (!source) {
802 return ERROR_INVALID_PARAM;
803 }
804
805 switch (source->type) {
808 return ASCIICHAT_OK; // No-op for webcam
809
811 if (!source->video_decoder || !source->audio_decoder) {
812 return ERROR_INVALID_PARAM;
813 }
814
815 // Lock shared decoder if YouTube URL (protect against concurrent thread access)
816 if (source->is_shared_decoder) {
817 mutex_lock(&source->decoder_mutex);
818 }
819
820 // Rewind video decoder
822 if (video_result != ASCIICHAT_OK) {
823 if (source->is_shared_decoder) {
824 mutex_unlock(&source->decoder_mutex);
825 }
826 return video_result;
827 }
828
829 // For YouTube (shared decoder), don't rewind audio separately
830 // For local files (separate decoders), rewind audio too
832 if (!source->is_shared_decoder) {
833 result = ffmpeg_decoder_rewind(source->audio_decoder);
834 }
835
836 // Unlock shared decoder
837 if (source->is_shared_decoder) {
838 mutex_unlock(&source->decoder_mutex);
839 }
840
841 return result;
842
844 return ERROR_NOT_SUPPORTED; // Cannot seek stdin
845
846 default:
847 return ERROR_INVALID_PARAM;
848 }
849}
asciichat_error_t ffmpeg_decoder_rewind(ffmpeg_decoder_t *decoder)
Seek to beginning of media.

References ASCIICHAT_OK, media_source_t::audio_decoder, media_source_t::decoder_mutex, ERROR_INVALID_PARAM, ERROR_NOT_SUPPORTED, ffmpeg_decoder_rewind(), media_source_t::is_shared_decoder, MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, mutex_lock, mutex_unlock, media_source_t::type, and media_source_t::video_decoder.

Referenced by media_source_read_audio(), and media_source_read_video().

◆ media_source_seek()

asciichat_error_t media_source_seek ( media_source_t *  source,
double  timestamp_sec 
)

#include <source.h>

Seek media source to timestamp.

Parameters
sourceMedia source (must not be NULL)
timestamp_secTimestamp in seconds
Returns
ASCIICHAT_OK on success, error code on failure

Seeks both video and audio decoders to the specified timestamp. Handles shared decoder locking for YouTube URLs.

Behavior:

  • FILE sources: Seek to timestamp (if seekable)
  • STDIN sources: ERROR_NOT_SUPPORTED (cannot seek stdin)
  • WEBCAM/TEST sources: No-op (always returns OK)

Thread safety:

  • For YouTube URLs with shared decoder, automatically locks during seek
  • Safe to call from any thread
Note
FILE sources only (returns OK for others)
STDIN sources: ERROR_NOT_SUPPORTED (cannot seek stdin)
Negative timestamps are rejected
Seeks are approximate (may land before requested timestamp)

Definition at line 851 of file source.c.

851 {
852 if (!source) {
853 SET_ERRNO(ERROR_INVALID_PARAM, "Media source is NULL");
854 return ERROR_INVALID_PARAM;
855 }
856
857 if (timestamp_sec < 0.0) {
858 SET_ERRNO(ERROR_INVALID_PARAM, "Timestamp must be >= 0.0");
859 return ERROR_INVALID_PARAM;
860 }
861
862 // WEBCAM and TEST sources: no-op (always return OK)
863 if (source->type == MEDIA_SOURCE_WEBCAM || source->type == MEDIA_SOURCE_TEST) {
864 return ASCIICHAT_OK;
865 }
866
867 // STDIN: not supported
868 if (source->type == MEDIA_SOURCE_STDIN) {
869 SET_ERRNO(ERROR_NOT_SUPPORTED, "Cannot seek stdin");
870 return ERROR_NOT_SUPPORTED;
871 }
872
874
875 // Clear audio playback buffer BEFORE seeking to prevent old audio from being queued
876 // This ensures fresh audio starts playing immediately after seek completes
877 if (source->audio_ctx) {
878 audio_context_t *audio_ctx = (audio_context_t *)source->audio_ctx;
879 if (audio_ctx->playback_buffer) {
880 audio_ring_buffer_clear(audio_ctx->playback_buffer);
881 }
882 }
883
884 // The seeking_in_progress flag in decoders blocks prefetch thread
885 // seeking_in_progress coordinates with audio callback via condition variable
886
887 uint64_t seek_start_ns = time_get_ns();
888 if (source->video_decoder) {
889 double video_pos_before = ffmpeg_decoder_get_position(source->video_decoder);
890 asciichat_error_t video_err = ffmpeg_decoder_seek_to_timestamp(source->video_decoder, timestamp_sec);
891 double video_pos_after = ffmpeg_decoder_get_position(source->video_decoder);
892 uint64_t video_seek_ns = time_elapsed_ns(seek_start_ns, time_get_ns());
893 char video_seek_str[32];
894 time_pretty(video_seek_ns, -1, video_seek_str, sizeof(video_seek_str));
895 if (video_err != ASCIICHAT_OK) {
896 log_warn("Video seek to %.2f failed: error code %d (took %s)", timestamp_sec, video_err, video_seek_str);
897 result = video_err;
898 } else {
899 log_info("Video SEEK: %.2f → %.2f sec (target %.2f, took %s)", video_pos_before, video_pos_after,
900 timestamp_sec, video_seek_str);
901 }
902 }
903
904 // Seek audio decoder (if separate from video)
905 if (source->audio_decoder && !source->is_shared_decoder) {
906 log_info("=== Starting audio seek to %.2f sec ===", timestamp_sec);
907 uint64_t audio_seek_start_ns = time_get_ns();
908 double audio_pos_before = ffmpeg_decoder_get_position(source->audio_decoder);
909 log_info("Audio position before seek: %.2f", audio_pos_before);
910 asciichat_error_t audio_err = ffmpeg_decoder_seek_to_timestamp(source->audio_decoder, timestamp_sec);
911 double audio_pos_after = ffmpeg_decoder_get_position(source->audio_decoder);
912 uint64_t audio_seek_ns = time_elapsed_ns(audio_seek_start_ns, time_get_ns());
913 log_info("Audio position after seek: %.2f", audio_pos_after);
914 char audio_seek_str[32];
915 time_pretty(audio_seek_ns, -1, audio_seek_str, sizeof(audio_seek_str));
916 if (audio_err != ASCIICHAT_OK) {
917 log_warn("Audio seek to %.2f failed: error code %d (took %s)", timestamp_sec, audio_err, audio_seek_str);
918 result = audio_err;
919 } else {
920 log_info("Audio SEEK COMPLETE: %.2f → %.2f sec (target %.2f, took %s)", audio_pos_before, audio_pos_after,
921 timestamp_sec, audio_seek_str);
922 }
923 }
924
925 // Prefetch thread automatically resumes when seeking_in_progress is cleared and signaled
926
927 return result;
928}
void audio_ring_buffer_clear(audio_ring_buffer_t *rb)
Clear all audio samples from ring buffer.
asciichat_error_t ffmpeg_decoder_seek_to_timestamp(ffmpeg_decoder_t *decoder, double timestamp_sec)
Seek to specific timestamp in media.
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as pretty duration with spaces and configurable precision.
Definition util/time.c:424
Audio context for full-duplex capture and playback.
void * audio_ctx
Audio context for clearing buffers on seek (opaque)
Definition source.c:59

References ASCIICHAT_OK, media_source_t::audio_ctx, media_source_t::audio_decoder, audio_ring_buffer_clear(), ERROR_INVALID_PARAM, ERROR_NOT_SUPPORTED, ffmpeg_decoder_get_position(), ffmpeg_decoder_seek_to_timestamp(), media_source_t::is_shared_decoder, log_info, log_warn, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, audio_context_t::playback_buffer, SET_ERRNO, time_elapsed_ns(), time_get_ns(), time_pretty(), media_source_t::type, and media_source_t::video_decoder.

Referenced by session_capture_create(), and session_handle_keyboard_input().

◆ media_source_set_audio_context()

void media_source_set_audio_context ( media_source_t *  source,
void *  audio_ctx 
)

#include <source.h>

Set audio context for playback buffer management.

Parameters
sourceMedia source (must not be NULL)
audio_ctxAudio context pointer (opaque, may be NULL)

Associates an audio context with the media source. Used to clear playback buffers during seeking to prevent audio lag.

Definition at line 1041 of file source.c.

1041 {
1042 if (source) {
1043 source->audio_ctx = audio_ctx;
1044 }
1045}

References media_source_t::audio_ctx.

Referenced by session_client_like_run().

◆ media_source_set_exit_callback()

void media_source_set_exit_callback ( media_source_t *  source,
bool(*)(void *)  should_exit_callback,
void *  user_data 
)

#include <source.h>

Set exit signal callback for graceful shutdown during I/O.

Parameters
sourceMedia source (must not be NULL)
callbackFunction that returns true if app should exit (can be NULL)
user_dataUser data to pass to callback (can be NULL)

Sets a callback on the underlying decoders that allows graceful shutdown when the application is shutting down (e.g., Ctrl+C). This is particularly important for file/HTTP sources where av_read_frame() might block indefinitely.

Purpose: When the user presses Ctrl+C, the signal handler sets the app's exit flag. This callback is polled by FFmpeg's interrupt handler to abort blocking I/O operations, allowing the prefetch thread to exit cleanly instead of hanging indefinitely.

Note
Called automatically from session_capture_create (no need to call manually)

Definition at line 1047 of file source.c.

1047 {
1048 if (!source) {
1049 return;
1050 }
1051
1052 // Set exit callback on video decoder if present
1053 if (source->video_decoder) {
1054 ffmpeg_decoder_set_exit_callback(source->video_decoder, should_exit_callback, user_data);
1055 }
1056
1057 // Set exit callback on audio decoder if present (only if different from video decoder)
1058 if (source->audio_decoder && source->audio_decoder != source->video_decoder) {
1059 ffmpeg_decoder_set_exit_callback(source->audio_decoder, should_exit_callback, user_data);
1060 }
1061}
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.

References media_source_t::audio_decoder, ffmpeg_decoder_set_exit_callback(), and media_source_t::video_decoder.

Referenced by session_capture_create().

◆ media_source_set_loop()

void media_source_set_loop ( media_source_t *  source,
bool  loop 
)

#include <source.h>

Enable or disable looping.

Parameters
sourceMedia source (must not be NULL)
looptrue to enable looping, false to disable

Enables loop mode for media files. When looping is enabled, the media source will automatically seek to the beginning when EOF is reached.

Behavior:

  • FILE sources: Seek to beginning on EOF (if seekable)
  • STDIN sources: Looping NOT supported (cannot seek stdin)
  • WEBCAM/TEST sources: Ignored (infinite by nature)
Note
For stdin, loop flag is silently ignored
Looping works for both video and audio streams

Definition at line 760 of file source.c.

760 {
761 if (!source) {
762 return;
763 }
764
765 source->loop_enabled = loop;
766
767 if (loop && source->type == MEDIA_SOURCE_STDIN) {
768 log_warn("Loop mode not supported for stdin input (cannot seek)");
769 }
770}

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

Referenced by session_capture_create().

◆ media_source_toggle_pause()

void media_source_toggle_pause ( media_source_t *  source)

#include <source.h>

Toggle pause state of media source.

Parameters
sourceMedia source (must not be NULL)

Toggles between paused and playing states. If paused, resumes playback. If playing, pauses playback.

Definition at line 1032 of file source.c.

1032 {
1033 if (!source) {
1034 return;
1035 }
1036 mutex_lock(&source->pause_mutex);
1037 source->is_paused = !source->is_paused;
1038 mutex_unlock(&source->pause_mutex);
1039}

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

Referenced by session_handle_keyboard_input().

◆ yt_dlp_extract_stream_url()

asciichat_error_t yt_dlp_extract_stream_url ( const char *  url,
const char *  yt_dlp_options,
char *  output_url,
size_t  output_size 
)

#include <yt_dlp.h>

Extract stream URL using yt-dlp with optional parameters.

Calls yt-dlp subprocess with –dump-json to extract direct stream URL. Results are cached for 30 seconds.

Parameters
urlInput URL (YouTube, Twitch, direct streams, etc.)
yt_dlp_optionsArbitrary yt-dlp options string e.g., "--cookies-from-browser=chrome --no-warnings" Pass NULL or "" for no extra options
output_urlOutput buffer for extracted stream URL
output_sizeSize of output buffer (recommend 8192+ bytes)
Returns
ASCIICHAT_OK on success, error code on failure
Note
On failure, logs error context via log_error()
Results are cached to avoid redundant yt-dlp calls
Thread-safe: Uses centralized cache with proper locking

Definition at line 150 of file yt_dlp.c.

151 {
152 if (!url || !output_url || output_size < 256) {
153 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for yt-dlp URL extraction");
154 return ERROR_INVALID_PARAM;
155 }
156
157 // Check if yt-dlp is available
158 if (!yt_dlp_is_available()) {
159 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp is not installed. Please install it with: "
160 "pip install yt-dlp (or: brew install yt-dlp on macOS)");
162 }
163
164 // Check if we have a cached extraction for this URL+options combination
165 char cached_url[8192] = {0};
166 if (yt_dlp_cache_get(url, yt_dlp_options, cached_url, sizeof(cached_url))) {
167 if (cached_url[0] != '\0') {
168 // Cached success - return the URL
169 SAFE_STRNCPY(output_url, cached_url, output_size - 1);
170 output_url[output_size - 1] = '\0';
171 return ASCIICHAT_OK;
172 } else {
173 // Cached failure - return error without logging again
175 }
176 }
177
178 // Build yt-dlp command
179 char command[4096];
180 const char *opts = yt_dlp_options ? yt_dlp_options : "";
181
182 int cmd_ret;
183 if (opts[0] != '\0') {
184 // User provided custom options
185 cmd_ret = safe_snprintf(command, sizeof(command),
186 "yt-dlp --quiet --no-warnings "
187 "--user-agent 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
188 "AppleWebKit/537.36' "
189 "%s "
190 "-f 'b' -O '%%(url)s' '%s' 2>&1",
191 opts, url);
192 } else {
193 // No custom options, use default
194 cmd_ret = safe_snprintf(command, sizeof(command),
195 "yt-dlp --quiet --no-warnings "
196 "--user-agent 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
197 "AppleWebKit/537.36' "
198 "-f 'b' -O '%%(url)s' '%s' 2>&1",
199 url);
200 }
201
202 if (cmd_ret < 0 || cmd_ret >= (int)sizeof(command)) {
203 SET_ERRNO(ERROR_INVALID_PARAM, "URL or yt-dlp options too long");
204 return ERROR_INVALID_PARAM;
205 }
206
207 log_debug("Executing yt-dlp: %s", command);
208
209 // Execute yt-dlp and capture output
210 FILE *pipe = NULL;
211 if (platform_popen("yt_dlp_extract", command, "r", &pipe) != ASCIICHAT_OK || !pipe) {
212 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "Failed to execute yt-dlp subprocess");
214 }
215
216 // Read output looking for stream URL (should be last line starting with http)
217 char url_buffer[8192] = {0};
218 char full_output[16384] = {0};
219 size_t ytdlp_output_len = 0;
220 size_t url_size = 0;
221 int c;
222
223 while ((c = fgetc(pipe)) != EOF && ytdlp_output_len < sizeof(full_output) - 1) {
224 full_output[ytdlp_output_len++] = (char)c;
225
226 // Track current line - looking for http:// or https:// stream URL
227 if ((url_size == 0 && c == 'h') || (url_size > 0 && c != '\n' && url_size < sizeof(url_buffer) - 1)) {
228 url_buffer[url_size++] = (char)c;
229 } else if (c == '\n') {
230 if (url_size > 0) {
231 url_buffer[url_size] = '\0';
232 if (url_is_valid(url_buffer)) {
233 break; // Found valid URL
234 }
235 }
236 url_size = 0; // Reset for next line
237 }
238 }
239 full_output[ytdlp_output_len] = '\0';
240 url_buffer[url_size] = '\0';
241
242 int pclose_ret = (platform_pclose(&pipe) == ASCIICHAT_OK) ? 0 : -1;
243 if (pclose_ret != 0) {
244 yt_dlp_cache_set(url, yt_dlp_options, NULL); // Cache failure
245
246 log_debug("yt-dlp exited with code %d", pclose_ret);
247 if (ytdlp_output_len > 0) {
248 log_error("yt-dlp output:\n%s", full_output);
249 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp failed to extract stream: %s", full_output);
250 } else {
251 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp failed to extract stream");
252 }
254 }
255
256 if (url_size == 0 || (url_size == 2 && strncmp(url_buffer, "NA", 2) == 0)) {
257 yt_dlp_cache_set(url, yt_dlp_options, NULL);
258 log_error("yt-dlp returned empty output for URL: %s", url);
259 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp returned no playable formats");
261 }
262
263 if (url_buffer[0] != 'h' || strncmp(url_buffer, "http", 4) != 0) {
264 yt_dlp_cache_set(url, yt_dlp_options, NULL);
265 log_error("Invalid URL from yt-dlp: %s (full output: %s)", url_buffer, full_output);
266 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp returned invalid URL");
268 }
269
270 if (url_size >= output_size) {
271 SET_ERRNO(ERROR_INVALID_PARAM, "Stream URL too long for output buffer (%zu bytes, max %zu)", url_size, output_size);
272 return ERROR_INVALID_PARAM;
273 }
274
275 SAFE_STRNCPY(output_url, url_buffer, output_size - 1);
276 output_url[output_size - 1] = '\0';
277
278 // Cache successful result
279 yt_dlp_cache_set(url, yt_dlp_options, output_url);
280
281 log_debug("yt-dlp successfully extracted stream URL (%zu bytes)", url_size);
282 return ASCIICHAT_OK;
283}
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
@ ERROR_YOUTUBE_EXTRACT_FAILED
bool yt_dlp_is_available(void)
Check if yt-dlp is installed and accessible.
Definition yt_dlp.c:143
asciichat_error_t platform_popen(const char *name, const char *command, const char *mode, FILE **out_stream)
Execute a command and return a file stream for reading/writing.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
asciichat_error_t platform_pclose(FILE **stream_ptr)
Close a process stream opened with platform_popen()

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_YOUTUBE_EXTRACT_FAILED, log_debug, log_error, platform_pclose(), platform_popen(), safe_snprintf(), SAFE_STRNCPY, SET_ERRNO, url_is_valid(), and yt_dlp_is_available().

◆ yt_dlp_is_available()

bool yt_dlp_is_available ( void  )

#include <yt_dlp.h>

Check if yt-dlp is installed and accessible.

Runs yt-dlp --version to verify yt-dlp is available in PATH.

Returns
true if yt-dlp –version succeeds, false otherwise
Note
Thread-safe: No static state

Definition at line 143 of file yt_dlp.c.

143 {
144 const char *argv[] = {"yt-dlp", "--version", NULL};
145 int ret = 0;
146 LOG_IO("yt-dlp", { ret = platform_execute_subprocess("yt-dlp", argv, NULL, 0); });
147 return (ret == 0);
148}
int platform_execute_subprocess(const char *executable, const char **argv, char *output_buffer, size_t output_size)
Execute a subprocess and optionally capture its output.

References LOG_IO, and platform_execute_subprocess().

Referenced by yt_dlp_extract_stream_url().