ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
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  ffmpeg_frame_status_t { FFMPEG_FRAME_READY , FFMPEG_FRAME_WAITING , FFMPEG_FRAME_EOF , FFMPEG_FRAME_FAILED }
 Decode next video frame. More...
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.
ffmpeg_frame_status_t ffmpeg_decoder_poll_video (ffmpeg_decoder_t *decoder, image_t **frame, asciichat_error_context_t *failure)
image_t * ffmpeg_decoder_read_video_frame (ffmpeg_decoder_t *decoder)
bool ffmpeg_decoder_has_video (ffmpeg_decoder_t *decoder)
 Check if decoder has video stream.
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_urls (const char *url, const char *yt_dlp_options, char *output_url, size_t output_size, char *audio_url, size_t audio_size)
 For yt dlp request options, this is the live record that ties proxy_specified together. The owning code creates it, updates it while work is active, and releases its handles and pointed-to storage before freeing the record.
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_H264 | 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_CAP_STEREO   ((uint32_t)(1u << 8))
 Default audio codec capabilities (all codecs supported).
#define VIDEO_CODEC_SUPPORTED(cap, codec)
 Check if a video codec is supported in capabilities bitmask.
#define AUDIO_CODEC_SUPPORTED(cap, codec)
 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:

  • Multi-format container support (mp4, avi, mkv, webm, etc.)
  • Video decoding to RGB24 format
  • Audio decoding to 48kHz interleaved stereo float
  • Stdin input support via custom AVIOContext
  • Seeking for loop support
  • Automatic stream detection

FFMPEG LIBRARIES USED:

  • libavformat: Container demuxing
  • libavcodec: Video/audio codec decoding
  • libavutil: Utilities and error handling
  • libswscale: Video format conversion (YUV -> RGB)
  • libswresample: Audio resampling (any rate -> 48kHz stereo)

SUPPORTED CODECS:

  • Video: H.264, H.265, VP8, VP9, AV1, MPEG-4, etc.
  • Audio: AAC, MP3, Opus, Vorbis, FLAC, PCM, etc.
  • Containers: MP4, AVI, MKV, WebM, MOV, FLV, GIF, etc.
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:

  • Unified API for webcam, files, and stdin
  • FFmpeg-based decoding for all common formats
  • Video frame extraction (RGB format)
  • Audio sample extraction (48kHz interleaved stereo float)
  • Loop support for file playback
  • Automatic format detection

SUPPORTED SOURCES:

  • MEDIA_SOURCE_WEBCAM: Hardware webcam device
  • MEDIA_SOURCE_FILE: Media file (mp4, avi, mkv, gif, mp3, etc.)
  • MEDIA_SOURCE_STDIN: Piped or redirected media data
  • MEDIA_SOURCE_TEST: Test pattern (existing)

SUPPORTED FORMATS:

  • Video: mp4, avi, mkv, webm, mov, flv, wmv, gif (animated)
  • Audio: mp3, aac, opus, flac, wav, ogg, m4a
  • Images: gif, png, jpg (static - single frame)

COMMAND-LINE EXAMPLES:

# Webcam preview with a deterministic source instead of a camera
ascii-chat mirror --test-pattern --color-mode 256
# Play a file repeatedly, starting at one minute and thirty seconds
ascii-chat mirror --file "demo recording.mp4" --loop --seek 01:30
# Decode a URL through the normal FFmpeg/yt-dlp resolver
ascii-chat mirror --url "https://example.com/clip.m3u8"
# Feed any FFmpeg-readable format through stdin
ffmpeg -i input.mkv -f matroska - | ascii-chat mirror --file -

PROGRAMMATIC EXAMPLE:

if (!source) return ERROR_MEDIA;
for (;;) {
if (frame) {
render_frame(frame);
continue;
}
// A temporary decode miss is not EOF; let the capture loop try again.
}
float samples[960]; // 20 ms of interleaved 48 kHz stereo float audio
size_t count = media_source_read_audio(source, samples, 960);
(void)count;
audio_visualization_source_t source
Definition fft.c:24
bool media_source_at_end(media_source_t *source)
Check if media source reached end of stream.
Definition source.c:836
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:722
void media_source_destroy(media_source_t *source)
Destroy media source and free resources.
Definition source.c:508
image_t * media_source_read_video(media_source_t *source)
Read next video frame from media source.
Definition source.c:592
media_source_t * media_source_create(media_source_type_t type, const char *path)
Create a new media source.
Definition source.c:221
void media_source_set_loop(media_source_t *source, bool loop)
Enable or disable looping.
Definition source.c:824
@ MEDIA_SOURCE_FILE
Media file (video/audio).
Definition source.h:99
SIMD-friendly storage for one RGB pixel. r, g, and b hold the color channels and padding keeps the la...
Media source for video and audio capture.
Definition source.c:47
Note
The CLI examples use the same media-source abstraction as the C API.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

Macro Definition Documentation

◆ AUDIO_CAP_STEREO

#define AUDIO_CAP_STEREO   ((uint32_t)(1u << 8))

#include <include/ascii-chat/media/codecs.h>

Default audio codec capabilities (all codecs supported).

Interleaved stereo PCM and stereo Opus output

Definition at line 79 of file codecs.h.

◆ AUDIO_CODEC_SUPPORTED

#define AUDIO_CODEC_SUPPORTED ( cap,
codec )

#include <include/ascii-chat/media/codecs.h>

Value:
(((cap) & (1 << (codec))) != 0)

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 98 of file codecs.h.

◆ VIDEO_CODEC_CAP_ALL

#include <include/ascii-chat/media/codecs.h>

Default video codec capabilities (all codecs supported).

Definition at line 52 of file codecs.h.

Referenced by session_protocol_build_capabilities().

◆ VIDEO_CODEC_SUPPORTED

#define VIDEO_CODEC_SUPPORTED ( cap,
codec )

#include <include/ascii-chat/media/codecs.h>

Value:
(((cap) & (1 << (codec))) != 0)

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 89 of file codecs.h.

Referenced by handle_image_frame_h265_packet(), and handle_image_frame_packet().

Typedef Documentation

◆ ffmpeg_decoder_t

typedef struct ffmpeg_decoder_t ffmpeg_decoder_t

#include <include/ascii-chat/media/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 59 of file ffmpeg_decoder.h.

◆ media_source_t

typedef struct media_source_t media_source_t

#include <include/ascii-chat/media/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 121 of file source.h.

Enumeration Type Documentation

◆ ffmpeg_frame_status_t

#include <include/ascii-chat/media/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
Enumerator
FFMPEG_FRAME_READY 
FFMPEG_FRAME_WAITING 
FFMPEG_FRAME_EOF 
FFMPEG_FRAME_FAILED 

Definition at line 229 of file ffmpeg_decoder.h.

ffmpeg_frame_status_t
Decode next video frame.
@ FFMPEG_FRAME_READY
@ FFMPEG_FRAME_WAITING
@ FFMPEG_FRAME_FAILED
@ FFMPEG_FRAME_EOF

◆ media_source_type_t

#include <include/ascii-chat/media/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 97 of file source.h.

97 {
media_source_type_t
Media source type enumeration.
Definition source.h:97
@ MEDIA_SOURCE_STDIN
Piped or redirected input.
Definition source.h:100
@ MEDIA_SOURCE_WEBCAM
Hardware webcam device.
Definition source.h:98
@ MEDIA_SOURCE_TEST
Test pattern generator.
Definition source.h:101

Function Documentation

◆ ffmpeg_decoder_at_end()

bool ffmpeg_decoder_at_end ( ffmpeg_decoder_t * decoder)

#include <include/ascii-chat/media/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 1782 of file ffmpeg_decoder.c.

1782 {
1783 return decoder && atomic_load_bool(&decoder->eof_reached);
1784}
atomic_t eof_reached
Whether end of file was reached.

References ffmpeg_decoder_t::eof_reached.

Referenced by ffmpeg_decoder_poll_video(), 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 <include/ascii-chat/media/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 769 of file ffmpeg_decoder.c.

769 {
770 return ffmpeg_decoder_create_interruptible(path, NULL, NULL);
771}
ffmpeg_decoder_t * ffmpeg_decoder_create_interruptible(const char *path, bool(*cancel)(void *), void *data)

References ffmpeg_decoder_create_interruptible().

◆ ffmpeg_decoder_create_stdin()

ffmpeg_decoder_t * ffmpeg_decoder_create_stdin ( void )

#include <include/ascii-chat/media/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 1043 of file ffmpeg_decoder.c.

1043 {
1044 ASCII_OTEL_SCOPE(FFMPEG_DECODER_CREATE_STDIN);
1045 // Buffer entire stdin on first call (shared between video and audio decoders)
1046 if (!g_stdin_buffer) {
1047 log_debug("Reading stdin into buffer...");
1048 g_stdin_buffer = stdin_buffer_read_all();
1049 if (!g_stdin_buffer) {
1050 SET_ERRNO(ERROR_MEDIA_OPEN, "Failed to read stdin into buffer");
1051 return NULL;
1052 }
1053 }
1054
1056 if (!decoder) {
1057 SET_ERRNO(ERROR_MEMORY, "Failed to allocate decoder");
1058 return NULL;
1059 }
1060
1061 memset(decoder, 0, sizeof(*decoder));
1062 decoder->video_stream_idx = -1;
1063 decoder->audio_stream_idx = -1;
1064 decoder->is_stdin = true;
1065 decoder->last_video_pts = -1.0;
1066 decoder->last_audio_pts = -1.0;
1067
1068 // Create per-decoder reader (each decoder gets independent position tracking)
1070 if (!reader) {
1071 SET_ERRNO(ERROR_MEMORY, "Failed to allocate buffer reader");
1072 SAFE_FREE(decoder);
1073 return NULL;
1074 }
1075 reader->buffer = g_stdin_buffer;
1076 reader->pos = 0; // Each decoder starts from position 0
1077
1078 // AVIO takes ownership of this buffer and may resize or free it with av_free().
1079 decoder->avio_buffer = av_malloc(AVIO_BUFFER_SIZE);
1080 if (!decoder->avio_buffer) {
1081 SET_ERRNO(ERROR_MEMORY, "Failed to allocate AVIO buffer");
1082 SAFE_FREE(reader);
1083 SAFE_FREE(decoder);
1084 return NULL;
1085 }
1086
1087 decoder->avio_ctx = avio_alloc_context(decoder->avio_buffer, // internal buffer for AVIO
1088 AVIO_BUFFER_SIZE, // buffer size
1089 0, // write_flag
1090 reader, // opaque (per-decoder reader)
1091 memory_read_packet,
1092 NULL, // write_packet
1093 memory_seek_packet // seek (memory is seekable)
1094 );
1095
1096 if (!decoder->avio_ctx) {
1097 SET_ERRNO(ERROR_MEMORY, "Failed to create AVIO context");
1098 av_free(decoder->avio_buffer);
1099 decoder->avio_buffer = NULL;
1100 SAFE_FREE(decoder);
1101 return NULL;
1102 }
1103
1104 // Mark AVIO context as seekable (memory buffer supports seeking)
1105 decoder->avio_ctx->seekable = AVIO_SEEKABLE_NORMAL;
1106
1107 // Allocate format context
1108 decoder->format_ctx = avformat_alloc_context();
1109 if (!decoder->format_ctx) {
1110 SET_ERRNO(ERROR_MEMORY, "Failed to allocate format context");
1111 av_freep(&decoder->avio_ctx->buffer);
1112 avio_context_free(&decoder->avio_ctx);
1113 SAFE_FREE(decoder);
1114 return NULL;
1115 }
1116
1117 decoder->format_ctx->pb = decoder->avio_ctx;
1118 decoder->format_ctx->flags |= AVFMT_FLAG_CUSTOM_IO; // Tell FFmpeg to use our custom AVIO context
1119
1120 // Capture FFmpeg's probing output
1121 int ret = 0;
1122 LOG_IO("ffmpeg", {
1123 // Open input from buffered stdin
1124 ret = traced_format_open(&decoder->format_ctx, NULL, NULL, NULL);
1125 if (ret >= 0) {
1126 // Find stream info
1127 if (traced_format_probe(decoder->format_ctx, NULL) < 0) {
1128 ret = -1;
1129 }
1130 }
1131 });
1132
1133 if (ret < 0) {
1134 if (ret == -1) {
1135 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to find stream info from stdin buffer");
1136 } else {
1137 SET_ERRNO(ERROR_MEDIA_OPEN, "Failed to open stdin buffer");
1138 }
1139 av_freep(&decoder->avio_ctx->buffer);
1140 avio_context_free(&decoder->avio_ctx);
1141 if (decoder->format_ctx) {
1142 avformat_free_context(decoder->format_ctx);
1143 }
1144 SAFE_FREE(decoder);
1145 return NULL;
1146 }
1147
1148 // Open codecs (same as file-based decoder)
1149 asciichat_error_t err = open_optional_codec_context(decoder->format_ctx, AVMEDIA_TYPE_VIDEO,
1150 &decoder->video_stream_idx, &decoder->video_codec_ctx);
1151 if (err != ASCIICHAT_OK) {
1152 ffmpeg_decoder_destroy(decoder);
1153 return NULL;
1154 }
1155
1156 if (GET_OPTION(audio_enabled)) {
1157 err = open_optional_codec_context(decoder->format_ctx, AVMEDIA_TYPE_AUDIO, &decoder->audio_stream_idx,
1158 &decoder->audio_codec_ctx);
1159 if (err != ASCIICHAT_OK) {
1160 ffmpeg_decoder_destroy(decoder);
1161 return NULL;
1162 }
1163 } else {
1164 decoder->audio_stream_idx = -1;
1165 decoder->audio_codec_ctx = NULL;
1166 log_debug("Audio decoding disabled by user option");
1167 }
1168
1169 if (decoder->video_stream_idx < 0 && decoder->audio_stream_idx < 0) {
1170 SET_ERRNO(ERROR_MEDIA_DECODE, "No video or audio streams found in stdin");
1171 ffmpeg_decoder_destroy(decoder);
1172 return NULL;
1173 }
1174
1175 // Allocate frame and packet
1176 decoder->frame = av_frame_alloc();
1177 decoder->packet = av_packet_alloc();
1178 if (!decoder->frame || !decoder->packet) {
1179 SET_ERRNO(ERROR_MEMORY, "Failed to allocate frame/packet");
1180 ffmpeg_decoder_destroy(decoder);
1181 return NULL;
1182 }
1183
1184 // Initialize swscale/swresample (same as file-based)
1185 if (decoder->video_codec_ctx) {
1186 // Validate codec context has valid dimensions and pixel format
1187 // For stdin/HTTP streams, these might not be valid until first frame is read
1188 if (decoder->video_codec_ctx->width <= 0 || decoder->video_codec_ctx->height <= 0) {
1189 log_warn("Video codec has invalid dimensions (%dx%d), will initialize swscale on first frame",
1190 decoder->video_codec_ctx->width, decoder->video_codec_ctx->height);
1191 // Don't create swscale context yet - will create it lazily on first frame read
1192 } else if (decoder->video_codec_ctx->pix_fmt == AV_PIX_FMT_NONE) {
1193 log_warn("Video codec has invalid pixel format, will initialize swscale on first frame");
1194 // Don't create swscale context yet - will create it lazily on first frame read
1195 } else {
1196 // Create swscale context with valid parameters
1197 LOG_IO("swscaler", {
1198 decoder->sws_ctx =
1199 sws_getContext(decoder->video_codec_ctx->width, decoder->video_codec_ctx->height,
1200 decoder->video_codec_ctx->pix_fmt, decoder->video_codec_ctx->width,
1201 decoder->video_codec_ctx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
1202 });
1203 if (!decoder->sws_ctx) {
1204 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to create swscale context");
1205 ffmpeg_decoder_destroy(decoder);
1206 return NULL;
1207 }
1208 }
1209 }
1210
1211 if (decoder->audio_codec_ctx) {
1212 decoder->swr_ctx = swr_alloc();
1213 if (!decoder->swr_ctx) {
1214 SET_ERRNO(ERROR_MEMORY, "Failed to allocate swresample context");
1215 ffmpeg_decoder_destroy(decoder);
1216 return NULL;
1217 }
1218
1219 av_opt_set_chlayout(decoder->swr_ctx, "in_chlayout", &decoder->audio_codec_ctx->ch_layout, 0);
1220 av_opt_set_int(decoder->swr_ctx, "in_sample_rate", decoder->audio_codec_ctx->sample_rate, 0);
1221 av_opt_set_sample_fmt(decoder->swr_ctx, "in_sample_fmt", decoder->audio_codec_ctx->sample_fmt, 0);
1222
1223 AVChannelLayout out_ch_layout = AV_CHANNEL_LAYOUT_STEREO;
1224 av_opt_set_chlayout(decoder->swr_ctx, "out_chlayout", &out_ch_layout, 0);
1225 av_opt_set_int(decoder->swr_ctx, "out_sample_rate", TARGET_SAMPLE_RATE, 0);
1226 av_opt_set_sample_fmt(decoder->swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
1227 if (decoder->audio_codec_ctx->ch_layout.nb_channels == 1) {
1228 const double matrix[] = {1.0, 1.0};
1229 swr_set_matrix(decoder->swr_ctx, matrix, 1);
1230 }
1231
1232 if (swr_init(decoder->swr_ctx) < 0) {
1233 SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to initialize swresample context");
1234 ffmpeg_decoder_destroy(decoder);
1235 return NULL;
1236 }
1237
1239 decoder->audio_buffer = SAFE_MALLOC(decoder->audio_buffer_size * sizeof(float), float *);
1240 if (!decoder->audio_buffer) {
1241 SET_ERRNO(ERROR_MEMORY, "Failed to allocate audio buffer");
1242 ffmpeg_decoder_destroy(decoder);
1243 return NULL;
1244 }
1245 }
1246
1247 // Initialize video frame prefetching system for stdin (same as file-based)
1248 if (decoder->video_stream_idx >= 0) {
1249 int width = decoder->video_codec_ctx->width;
1250 int height = decoder->video_codec_ctx->height;
1251
1252 // For stdin sources, dimensions might be invalid initially (0x0)
1253 // Use default dimensions for prefetch buffers; they'll be resized on first frame
1254 if (width <= 0 || height <= 0) {
1255 width = 1920; // Default width for stdin
1256 height = 1080; // Default height for stdin
1257 log_debug("stdin: Using default prefetch dimensions %dx%d (will be resized on first frame)", width, height);
1258 }
1259
1260 // Create two prefetch image buffers for double-buffering
1261 decoder->prefetch_image_a = image_new((size_t)width, (size_t)height);
1262 decoder->prefetch_image_b = image_new((size_t)width, (size_t)height);
1263 if (!decoder->prefetch_image_a || !decoder->prefetch_image_b) {
1264 SET_ERRNO(ERROR_MEMORY, "Failed to allocate prefetch image buffers");
1265 ffmpeg_decoder_destroy(decoder);
1266 return NULL;
1267 }
1268
1269 decoder->current_prefetch_image = decoder->prefetch_image_a;
1270 decoder->prefetch_frame_ready = false;
1271
1272 // Initialize prefetch mutex
1273 if (mutex_init(&decoder->prefetch_mutex, "ffmpeg_prefetch") != 0) {
1274 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch mutex");
1275 ffmpeg_decoder_destroy(decoder);
1276 return NULL;
1277 }
1278
1279 if (cond_init(&decoder->prefetch_cond, "ffmpeg_prefetch") != 0) {
1280 SET_ERRNO(ERROR_MEMORY, "Failed to initialize prefetch condition variable");
1281 mutex_destroy(&decoder->prefetch_mutex);
1282 ffmpeg_decoder_destroy(decoder);
1283 return NULL;
1284 }
1285
1286 if (mutex_init(&decoder->read_frame_mutex, "ffmpeg_read_frame") != 0) {
1287 SET_ERRNO(ERROR_MEMORY, "Failed to initialize read_frame mutex");
1288 mutex_destroy(&decoder->prefetch_mutex);
1289 cond_destroy(&decoder->prefetch_cond);
1290 ffmpeg_decoder_destroy(decoder);
1291 return NULL;
1292 }
1293 }
1294
1295 log_debug("FFmpeg decoder opened from stdin (video=%s, audio=%s)", decoder->video_stream_idx >= 0 ? "yes" : "no",
1296 decoder->audio_stream_idx >= 0 ? "yes" : "no");
1297
1298 NAMED_REGISTER_FFMPEG_DECODER(decoder, "stdin", NULL);
1299 NAMED_REGISTER_ATOMIC(&decoder->eof_reached, "ffmpeg_eof", decoder);
1300
1301 return decoder;
1302}
#define SET_ERRNO(code,...)
#define TARGET_CHANNELS
#define AVIO_BUFFER_SIZE
#define TARGET_SAMPLE_RATE
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t with automatic format specifier.
#define NAMED_REGISTER_FFMPEG_DECODER(decoder, name, parent_ptr)
Register an FFmpeg decoder with automatic format specifier.
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_MEDIA_DECODE
Definition error_codes.h:67
@ ERROR_MEDIA_OPEN
Definition error_codes.h:66
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
void ffmpeg_decoder_destroy(ffmpeg_decoder_t *decoder)
Destroy FFmpeg decoder and free resources.
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
#define LOG_IO(prefix, block)
Definition io.h:104
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
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.
image_t * prefetch_image_b
Second prefetch buffer.
cond_t prefetch_cond
Condition variable for pausing during seek.
AVIOContext * avio_ctx
Custom I/O context for stdin.
mutex_t read_frame_mutex
Protect traced_packet_read() calls from video and audio threads.
struct SwrContext * swr_ctx
Software resampler for format conversion.
AVPacket * packet
Reusable packet for reading.
int video_stream_idx
Video stream index (-1 if none).
unsigned char * avio_buffer
Buffer for custom I/O.
double last_audio_pts
Last audio presentation timestamp.
AVCodecContext * audio_codec_ctx
Audio codec context.
bool is_stdin
Whether reading from stdin.
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.
_Atomic bool prefetch_frame_ready
Whether current_prefetch_image has valid data.
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.
size_t pos
Current read position (per-decoder).
stdin_buffer_t * buffer
Pointer to shared buffer.

References ASCII_OTEL_SCOPE, 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, ffmpeg_decoder_t::current_prefetch_image, ffmpeg_decoder_t::eof_reached, ERROR_MEDIA_DECODE, ERROR_MEDIA_OPEN, ERROR_MEMORY, ffmpeg_decoder_destroy(), ffmpeg_decoder_t::format_ctx, ffmpeg_decoder_t::frame, GET_OPTION, height, 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, NAMED_REGISTER_ATOMIC, 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_CHANNELS, TARGET_SAMPLE_RATE, ffmpeg_decoder_t::video_codec_ctx, ffmpeg_decoder_t::video_stream_idx, and width.

Referenced by media_source_create_interruptible().

◆ ffmpeg_decoder_destroy()

void ffmpeg_decoder_destroy ( ffmpeg_decoder_t * decoder)

#include <include/ascii-chat/media/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 1304 of file ffmpeg_decoder.c.

1304 {
1305 ASCII_OTEL_SCOPE(FFMPEG_DECODER_DESTROY);
1306 if (!decoder) {
1307 return;
1308 }
1309
1310 NAMED_UNREGISTER(&decoder->eof_reached);
1311 NAMED_UNREGISTER(decoder);
1312
1313 // Stop prefetch thread (signal it to stop and wait for it to finish)
1314 if (decoder->prefetch_thread_running) {
1315 mutex_lock(&decoder->prefetch_mutex);
1316 decoder->prefetch_should_stop = true;
1317 cond_signal(&decoder->prefetch_cond);
1318 mutex_unlock(&decoder->prefetch_mutex);
1319
1320 // Wait for thread to finish
1321 thread_join(&decoder->prefetch_thread, NULL);
1322 decoder->prefetch_thread_running = false;
1323 }
1324
1325 // Clean up prefetch state
1326 cond_destroy(&decoder->prefetch_cond);
1327 mutex_destroy(&decoder->prefetch_mutex);
1328 mutex_destroy(&decoder->read_frame_mutex);
1329
1330 // Free prefetch image buffers
1331 if (decoder->prefetch_image_a) {
1333 decoder->prefetch_image_a = NULL;
1334 }
1335 if (decoder->prefetch_image_b) {
1337 decoder->prefetch_image_b = NULL;
1338 }
1339
1340 // Don't destroy current_image - it points to one of the prefetch buffers
1341 // which have already been destroyed above
1342 decoder->current_image = NULL;
1343
1344 // Free audio buffer
1345 SAFE_FREE(decoder->audio_buffer);
1346
1347 // Free swscale context
1348 if (decoder->sws_ctx) {
1349 sws_freeContext(decoder->sws_ctx);
1350 decoder->sws_ctx = NULL;
1351 }
1352
1353 // Free swresample context
1354 if (decoder->swr_ctx) {
1355 swr_free(&decoder->swr_ctx);
1356 }
1357
1358 // Free frame and packet
1359 if (decoder->frame) {
1360 av_frame_free(&decoder->frame);
1361 }
1362 if (decoder->packet) {
1363 av_packet_free(&decoder->packet);
1364 }
1365
1366 // Free codec contexts
1367 if (decoder->video_codec_ctx) {
1368 avcodec_free_context(&decoder->video_codec_ctx);
1369 }
1370 if (decoder->audio_codec_ctx) {
1371 avcodec_free_context(&decoder->audio_codec_ctx);
1372 }
1373
1374 // Free format context
1375 if (decoder->format_ctx) {
1376 avformat_close_input(&decoder->format_ctx);
1377 }
1378
1379 // Free AVIO context (stdin only)
1380 if (decoder->avio_ctx) {
1381 // Free the opaque reader for stdin
1382 if (decoder->is_stdin) {
1383 SAFE_FREE(decoder->avio_ctx->opaque);
1384 }
1385 av_freep(&decoder->avio_ctx->buffer);
1386 avio_context_free(&decoder->avio_ctx);
1387 }
1388
1389 SAFE_FREE(decoder);
1390}
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
void image_destroy(image_t *image)
Destroy an image allocated with image_new().
platform_thread_t prefetch_thread
Prefetch thread handle.
bool prefetch_thread_running
Whether prefetch thread is active.
_Atomic bool prefetch_should_stop
Signal to stop prefetch thread.
image_t * current_image
Working buffer for decoding.

References ASCII_OTEL_SCOPE, ffmpeg_decoder_t::audio_buffer, ffmpeg_decoder_t::audio_codec_ctx, ffmpeg_decoder_t::avio_ctx, ffmpeg_decoder_t::current_image, ffmpeg_decoder_t::eof_reached, ffmpeg_decoder_t::format_ctx, ffmpeg_decoder_t::frame, image_destroy(), ffmpeg_decoder_t::is_stdin, 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_stdin(), ffmpeg_decoder_create_with_request_options(), http_renderer_render_file(), media_source_create_interruptible(), and media_source_destroy().

◆ ffmpeg_decoder_get_duration()

double ffmpeg_decoder_get_duration ( ffmpeg_decoder_t * decoder)

#include <include/ascii-chat/media/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 1786 of file ffmpeg_decoder.c.

1786 {
1787 if (!decoder || !decoder->format_ctx) {
1788 return -1.0;
1789 }
1790
1791 if (decoder->format_ctx->duration == AV_NOPTS_VALUE) {
1792 return -1.0;
1793 }
1794
1795 return (double)decoder->format_ctx->duration / AV_TIME_BASE;
1796}

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 <include/ascii-chat/media/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 1798 of file ffmpeg_decoder.c.

1798 {
1799 if (!decoder) {
1800 return -1.0;
1801 }
1802
1803 // Prefer sample-based position tracking (continuous, works before frames are decoded)
1804 if (decoder->audio_sample_rate > 0 && decoder->audio_samples_read >= 0) {
1805 return (double)decoder->audio_samples_read / (double)decoder->audio_sample_rate;
1806 }
1807
1808 // Fallback to frame-based position if available
1809 if (decoder->last_video_pts >= 0.0) {
1810 return decoder->last_video_pts;
1811 } else if (decoder->last_audio_pts >= 0.0) {
1812 return decoder->last_audio_pts;
1813 }
1814
1815 return -1.0;
1816}
uint64_t audio_samples_read
Total audio samples decoded and output.
int audio_sample_rate
Audio sample rate (Hz).

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 <include/ascii-chat/media/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 1529 of file ffmpeg_decoder.c.

1529 {
1530 if (!decoder || decoder->video_stream_idx < 0) {
1531 return SET_ERRNO(ERROR_INVALID_PARAM, "Decoder has no video stream");
1532 }
1533
1534 if (width) {
1535 *width = decoder->video_codec_ctx->width;
1536 }
1537 if (height) {
1538 *height = decoder->video_codec_ctx->height;
1539 }
1540
1541 return ASCIICHAT_OK;
1542}
@ ERROR_INVALID_PARAM

References ASCIICHAT_OK, ERROR_INVALID_PARAM, height, SET_ERRNO, ffmpeg_decoder_t::video_codec_ctx, ffmpeg_decoder_t::video_stream_idx, and width.

◆ ffmpeg_decoder_get_video_fps()

double ffmpeg_decoder_get_video_fps ( ffmpeg_decoder_t * decoder)

#include <include/ascii-chat/media/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 1544 of file ffmpeg_decoder.c.

1544 {
1545 if (!decoder || decoder->video_stream_idx < 0) {
1546 return -1.0;
1547 }
1548
1549 AVStream *stream = decoder->format_ctx->streams[decoder->video_stream_idx];
1550
1551 // Try avg_frame_rate first (average frame rate from entire stream)
1552 double fps = av_q2d_safe(stream->avg_frame_rate);
1553
1554 // Fallback to r_frame_rate if avg_frame_rate is invalid or zero
1555 // r_frame_rate is the "real" frame rate based on codec parameters
1556 // This is more reliable for YouTube videos and some video codecs
1557 if (fps <= 0.0) {
1558 fps = av_q2d_safe(stream->r_frame_rate);
1559 }
1560
1561 return fps;
1562}

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 <include/ascii-chat/media/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 1692 of file ffmpeg_decoder.c.

1692 {
1693 return decoder && decoder->audio_stream_idx >= 0;
1694}

References ffmpeg_decoder_t::audio_stream_idx.

Referenced by media_source_create_interruptible(), and media_source_has_audio().

◆ ffmpeg_decoder_has_video()

bool ffmpeg_decoder_has_video ( ffmpeg_decoder_t * decoder)

#include <include/ascii-chat/media/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 1525 of file ffmpeg_decoder.c.

1525 {
1526 return decoder && decoder->video_stream_idx >= 0;
1527}

References ffmpeg_decoder_t::video_stream_idx.

Referenced by media_source_create_interruptible(), and media_source_has_video().

◆ ffmpeg_decoder_is_prefetch_running()

bool ffmpeg_decoder_is_prefetch_running ( ffmpeg_decoder_t * decoder)

#include <include/ascii-chat/media/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 1518 of file ffmpeg_decoder.c.

1518 {
1519 if (!decoder) {
1520 return false;
1521 }
1522 return decoder->prefetch_thread_running;
1523}

References ffmpeg_decoder_t::prefetch_thread_running.

◆ ffmpeg_decoder_poll_video()

ffmpeg_frame_status_t ffmpeg_decoder_poll_video ( ffmpeg_decoder_t * decoder,
image_t ** frame,
asciichat_error_context_t * failure )

#include <include/ascii-chat/media/ffmpeg_decoder.h>

Failure is an owned snapshot; no worker stack or thread lifetime dependency.

Definition at line 1396 of file ffmpeg_decoder.c.

1397 {
1398 ASCII_OTEL_SCOPE(FFMPEG_DECODER_POLL_VIDEO);
1399 if (frame)
1400 *frame = NULL;
1401 if (failure)
1402 memset(failure, 0, sizeof(*failure));
1403 if (!decoder || !frame || !failure) {
1405 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid video poll outputs");
1406 if (failure && !errno_peek_since(scope, failure))
1407 failure->code = ERROR_INVALID_PARAM;
1408 return FFMPEG_FRAME_FAILED;
1409 }
1410 // Empty polls need no protected state. EOF and errors still confirm under
1411 // the mutex so a final pending frame takes precedence over termination.
1412 if (!atomic_load_explicit(&decoder->prefetch_frame_ready, memory_order_acquire) &&
1413 !atomic_load_explicit(&decoder->prefetch_failed, memory_order_acquire) && !ffmpeg_decoder_at_end(decoder))
1414 return FFMPEG_FRAME_WAITING;
1415
1416 mutex_lock(&decoder->prefetch_mutex);
1417 if (decoder->prefetch_frame_ready && decoder->current_prefetch_image) {
1418 // Taking the next image releases the previous reader-owned buffer.
1419 if (decoder->current_read_buffer == decoder->prefetch_image_a)
1420 decoder->buffer_a_in_use = false;
1421 else if (decoder->current_read_buffer == decoder->prefetch_image_b)
1422 decoder->buffer_b_in_use = false;
1423 *frame = decoder->current_prefetch_image;
1424 decoder->prefetch_frame_ready = false;
1425 if (*frame == decoder->prefetch_image_a)
1426 decoder->buffer_a_in_use = true;
1427 else if (*frame == decoder->prefetch_image_b)
1428 decoder->buffer_b_in_use = true;
1429 decoder->current_read_buffer = *frame;
1430 decoder->current_image = *frame;
1431 cond_signal(&decoder->prefetch_cond);
1432 mutex_unlock(&decoder->prefetch_mutex);
1433 return FFMPEG_FRAME_READY;
1434 }
1435 *failure = decoder->prefetch_failure;
1436 bool eof = ffmpeg_decoder_at_end(decoder);
1437 mutex_unlock(&decoder->prefetch_mutex);
1438 if (failure->code != ASCIICHAT_OK)
1439 return FFMPEG_FRAME_FAILED;
1441}
bool errno_peek_since(asciichat_errno_scope_t scope, asciichat_error_context_t *out)
asciichat_errno_scope_t errno_checkpoint(void)
bool ffmpeg_decoder_at_end(ffmpeg_decoder_t *decoder)
Check if decoder reached end of stream.
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...
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.
_Atomic bool prefetch_failed
bool buffer_a_in_use
Whether prefetch_image_a is being read by main thread.
asciichat_error_context_t prefetch_failure

References ASCII_OTEL_SCOPE, ASCIICHAT_OK, ffmpeg_decoder_t::buffer_a_in_use, ffmpeg_decoder_t::buffer_b_in_use, asciichat_error_context_t::code, ffmpeg_decoder_t::current_image, ffmpeg_decoder_t::current_prefetch_image, ffmpeg_decoder_t::current_read_buffer, errno_checkpoint(), errno_peek_since(), ERROR_INVALID_PARAM, ffmpeg_decoder_at_end(), FFMPEG_FRAME_EOF, FFMPEG_FRAME_FAILED, FFMPEG_FRAME_READY, FFMPEG_FRAME_WAITING, ffmpeg_decoder_t::prefetch_cond, ffmpeg_decoder_t::prefetch_failed, ffmpeg_decoder_t::prefetch_failure, ffmpeg_decoder_t::prefetch_frame_ready, ffmpeg_decoder_t::prefetch_image_a, ffmpeg_decoder_t::prefetch_image_b, ffmpeg_decoder_t::prefetch_mutex, and SET_ERRNO.

Referenced by ffmpeg_decoder_read_video_frame(), and http_renderer_render_file().

◆ ffmpeg_decoder_read_audio_samples()

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

#include <include/ascii-chat/media/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 interleaved stereo float format, compatible with ascii-chat's Opus encoding.

Audio format:

  • Format: 32-bit float
  • Channels: Stereo (2 channels); sample counts include both channels
  • 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 stereo using swresample
  5. Copy to output buffer
Note
Skips non-audio packets automatically
May return fewer samples than requested at EOF

Definition at line 1568 of file ffmpeg_decoder.c.

1568 {
1569 ASCII_OTEL_SCOPE(FFMPEG_DECODER_READ_AUDIO_SAMPLES);
1570 // Keep channels and the playback clock aligned even with odd caller capacity.
1571 num_samples -= num_samples % TARGET_CHANNELS;
1572 if (!decoder || decoder->audio_stream_idx < 0 || !buffer || num_samples == 0) {
1573 return 0;
1574 }
1575
1576 mutex_lock(&decoder->read_frame_mutex);
1577 size_t samples_written = 0;
1578
1579 // First, drain any buffered samples
1580 if (decoder->audio_buffer_offset > 0) {
1581 size_t available = decoder->audio_buffer_offset;
1582 size_t to_copy = (available < num_samples) ? available : num_samples;
1583
1584 memcpy(buffer, decoder->audio_buffer, to_copy * sizeof(float));
1585 samples_written += to_copy;
1586
1587 // Shift buffer
1588 if (to_copy < available) {
1589 memmove(decoder->audio_buffer, decoder->audio_buffer + to_copy, (available - to_copy) * sizeof(float));
1590 }
1591 decoder->audio_buffer_offset -= to_copy;
1592
1593 if (samples_written >= num_samples) {
1594 decoder->audio_samples_read += samples_written / TARGET_CHANNELS;
1595 mutex_unlock(&decoder->read_frame_mutex);
1596 return samples_written;
1597 }
1598 }
1599
1600 // Read more packets to fill the request
1601 static uint64_t packet_count = 0;
1602
1603 while (samples_written < num_samples) {
1604 int ret = traced_packet_read(decoder->format_ctx, decoder->packet);
1605 if (ret < 0) {
1606 if (ret == AVERROR_EOF) {
1607 atomic_store_bool(&decoder->eof_reached, true);
1608 }
1609 break;
1610 }
1611
1612 // Check if this is an audio packet
1613 if (decoder->packet->stream_index != decoder->audio_stream_idx) {
1614 av_packet_unref(decoder->packet);
1615 continue;
1616 }
1617
1618 packet_count++;
1619 log_debug_every(US_PER_SEC_INT, "Audio packet #%lu: pts=%ld dts=%ld duration=%d size=%d", packet_count,
1620 decoder->packet->pts, decoder->packet->dts, decoder->packet->duration, decoder->packet->size);
1621
1622 // Send packet to decoder
1623 ret = traced_packet_send(decoder->audio_codec_ctx, decoder->packet);
1624 av_packet_unref(decoder->packet);
1625
1626 if (ret < 0) {
1627 char error_message[AV_ERROR_MAX_STRING_SIZE];
1628 av_strerror(ret, error_message, sizeof(error_message));
1629 log_warn_every(US_PER_SEC_INT, "Error sending audio packet to decoder: %s", error_message);
1630 continue;
1631 }
1632
1633 // Receive all decoded frames from this packet
1634 // Important: a single packet can produce multiple frames. Must drain all before next packet.
1635 while (1) {
1636 ret = traced_frame_receive(decoder->audio_codec_ctx, decoder->frame);
1637 if (ret == AVERROR(EAGAIN)) {
1638 break; // No more frames from this packet, get next packet
1639 } else if (ret < 0) {
1640 log_warn_every(US_PER_SEC_INT, "Error receiving audio frame from decoder");
1641 goto audio_read_done;
1642 }
1643
1644 // Update position tracking
1645 decoder->last_audio_pts =
1646 get_frame_pts_seconds(decoder->frame, decoder->format_ctx->streams[decoder->audio_stream_idx]->time_base);
1647
1648 // Resample the complete frame and retain output beyond this callback's
1649 // requested block. Flushing the resampler between packets would insert
1650 // padding into inputs whose sample rate differs from the output rate.
1651 uint8_t *out_ptr = (uint8_t *)decoder->audio_buffer;
1652 int converted =
1653 traced_audio_resample(decoder->swr_ctx, &out_ptr, (int)(decoder->audio_buffer_size / TARGET_CHANNELS),
1654 (const uint8_t **)decoder->frame->data, decoder->frame->nb_samples);
1655
1656 if (converted > 0) {
1657 size_t available = (size_t)converted * TARGET_CHANNELS;
1658 size_t remaining = num_samples - samples_written;
1659 size_t to_copy = available < remaining ? available : remaining;
1660 memcpy(buffer + samples_written, decoder->audio_buffer, to_copy * sizeof(float));
1661 samples_written += to_copy;
1662 decoder->audio_buffer_offset = available - to_copy;
1663 if (decoder->audio_buffer_offset > 0)
1664 memmove(decoder->audio_buffer, decoder->audio_buffer + to_copy, decoder->audio_buffer_offset * sizeof(float));
1665 }
1666
1667 if (samples_written >= num_samples) {
1668 goto audio_read_done;
1669 }
1670 }
1671 }
1672
1673audio_read_done:
1674 // Flush resampler buffer if we haven't filled the full request
1675 // The resampler may have buffered samples that need to be output
1676 if (samples_written < num_samples && atomic_load_bool(&decoder->eof_reached)) {
1677 int remaining_space = (int)((num_samples - samples_written) / TARGET_CHANNELS);
1678 uint8_t *out_ptr = (uint8_t *)(buffer + samples_written);
1679 int flushed = traced_audio_resample(decoder->swr_ctx, &out_ptr, remaining_space, NULL, 0);
1680 if (flushed > 0) {
1681 samples_written += (size_t)flushed * TARGET_CHANNELS;
1682 }
1683 }
1684
1685 // Update sample-based position tracking
1686 decoder->audio_samples_read += samples_written / TARGET_CHANNELS;
1687 mutex_unlock(&decoder->read_frame_mutex);
1688
1689 return samples_written;
1690}
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
#define US_PER_SEC_INT
Definition time.h:173
#define EAGAIN
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:749
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:755
size_t audio_buffer_offset
Current offset in audio buffer.

References ASCII_OTEL_SCOPE, 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_debug_every, log_warn_every, ffmpeg_decoder_t::packet, ffmpeg_decoder_t::read_frame_mutex, ffmpeg_decoder_t::swr_ctx, TARGET_CHANNELS, and US_PER_SEC_INT.

Referenced by media_source_read_audio().

◆ ffmpeg_decoder_read_video_frame()

image_t * ffmpeg_decoder_read_video_frame ( ffmpeg_decoder_t * decoder)

#include <include/ascii-chat/media/ffmpeg_decoder.h>

Definition at line 1443 of file ffmpeg_decoder.c.

1443 {
1444 ASCII_OTEL_SCOPE(FFMPEG_DECODER_READ_VIDEO_FRAME);
1445 image_t *frame = NULL;
1446 asciichat_error_context_t failure = {0};
1447 if (ffmpeg_decoder_poll_video(decoder, &frame, &failure) == FFMPEG_FRAME_FAILED)
1448 errno_import(&failure);
1449 return frame;
1450}
asciichat_error_t errno_import(const asciichat_error_context_t *cause)
ffmpeg_frame_status_t ffmpeg_decoder_poll_video(ffmpeg_decoder_t *decoder, image_t **frame, asciichat_error_context_t *failure)

References ASCII_OTEL_SCOPE, errno_import(), ffmpeg_decoder_poll_video(), and FFMPEG_FRAME_FAILED.

Referenced by media_source_read_video().

◆ ffmpeg_decoder_rewind()

asciichat_error_t ffmpeg_decoder_rewind ( ffmpeg_decoder_t * decoder)

#include <include/ascii-chat/media/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 1700 of file ffmpeg_decoder.c.

1700 {
1701 return ffmpeg_decoder_seek_to_timestamp(decoder, 0);
1702}
asciichat_error_t ffmpeg_decoder_seek_to_timestamp(ffmpeg_decoder_t *decoder, double timestamp_sec)
Seek to specific timestamp in media.

References ffmpeg_decoder_seek_to_timestamp().

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 <include/ascii-chat/media/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 1704 of file ffmpeg_decoder.c.

1704 {
1705 ASCII_OTEL_SCOPE(FFMPEG_DECODER_SEEK_TO_TIMESTAMP);
1706 if (!decoder) {
1707 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing decoder for seek");
1708 }
1709
1710 if (decoder->is_stdin) {
1711 return ERROR_NOT_SUPPORTED; // Cannot seek stdin
1712 }
1713
1714 // Hold mutex during entire seek operation to prevent race with prefetch thread
1715 mutex_lock(&decoder->prefetch_mutex);
1716
1717 // Set flag to pause prefetch thread via condition variable
1718 decoder->seeking_in_progress = true;
1719 ++decoder->seek_generation;
1720 // Serialize codec and partial-audio state with the existing packet reader.
1721 mutex_lock(&decoder->read_frame_mutex);
1722 // The old reader has stopped. Holding both guards prevents another read;
1723 // the seek must not cancel its own I/O through the interrupt callback.
1724 decoder->seeking_in_progress = false;
1725 if (decoder->format_ctx->pb && decoder->format_ctx->pb->error == AVERROR_EXIT)
1726 decoder->format_ctx->pb->error = 0;
1727
1728 // Convert seconds to FFmpeg time base units (AV_TIME_BASE = 1,000,000)
1729 int64_t target_ts = (int64_t)(timestamp_sec * AV_TIME_BASE);
1730
1731 // For HTTP streams, use simple keyframe seeking (faster than frame-accurate seeking)
1732 // HTTP seeking is expensive and can break stream state, so prefer speed over precision
1733 int seek_ret = av_seek_frame(decoder->format_ctx, -1, target_ts, AVSEEK_FLAG_BACKWARD);
1734 if (seek_ret < 0) {
1735 // Fallback: try without backward flag
1736 seek_ret = av_seek_frame(decoder->format_ctx, -1, target_ts, 0);
1737 }
1738
1739 if (seek_ret < 0) {
1740 decoder->seeking_in_progress = false;
1741 mutex_unlock(&decoder->read_frame_mutex);
1742 cond_signal(&decoder->prefetch_cond);
1743 mutex_unlock(&decoder->prefetch_mutex);
1744 return SET_ERRNO(ERROR_MEDIA_SEEK, "Failed to seek to timestamp %.2f seconds", timestamp_sec);
1745 }
1746
1747 // Flush codec buffers AFTER seeking
1748 if (decoder->video_codec_ctx) {
1749 avcodec_flush_buffers(decoder->video_codec_ctx);
1750 }
1751 if (decoder->audio_codec_ctx) {
1752 avcodec_flush_buffers(decoder->audio_codec_ctx);
1753 }
1754
1755 // Reset state
1756 atomic_store_bool(&decoder->eof_reached, false);
1757 decoder->video_draining = false;
1758 decoder->audio_buffer_offset = 0;
1759 // Clear any stale audio data in buffer
1760 if (decoder->audio_buffer) {
1761 memset(decoder->audio_buffer, 0, decoder->audio_buffer_size * sizeof(float));
1762 }
1763 decoder->last_video_pts = -1.0;
1764 decoder->last_audio_pts = -1.0;
1765 decoder->prefetch_frame_ready = false;
1766 // Set audio_samples_read to match the seek target so position tracking works correctly
1767 decoder->audio_samples_read = (uint64_t)(timestamp_sec * decoder->audio_sample_rate);
1768
1769 // The consumer retains its image across seeks until it takes a replacement.
1770
1771 // Resume prefetch thread
1772 decoder->seeking_in_progress = false;
1773 mutex_unlock(&decoder->read_frame_mutex);
1774 cond_signal(&decoder->prefetch_cond);
1775
1776 // Release mutex - prefetch thread can resume
1777 mutex_unlock(&decoder->prefetch_mutex);
1778
1779 return ASCIICHAT_OK;
1780}
@ ERROR_MEDIA_SEEK
Definition error_codes.h:68
@ ERROR_NOT_SUPPORTED
Definition error_codes.h:69
_Atomic bool seeking_in_progress
Signal to pause prefetch thread during seek.
bool video_draining
Whether EOF was sent to the video decoder.
_Atomic uint64_t seek_generation
Detect seeks before and after packet reads.

References ASCII_OTEL_SCOPE, 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::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, ffmpeg_decoder_t::prefetch_cond, ffmpeg_decoder_t::prefetch_frame_ready, ffmpeg_decoder_t::prefetch_mutex, ffmpeg_decoder_t::read_frame_mutex, ffmpeg_decoder_t::seek_generation, ffmpeg_decoder_t::seeking_in_progress, SET_ERRNO, ffmpeg_decoder_t::video_codec_ctx, and ffmpeg_decoder_t::video_draining.

Referenced by ffmpeg_decoder_rewind(), and media_source_seek().

◆ ffmpeg_decoder_set_exit_callback()

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

#include <include/ascii-chat/media/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 1509 of file ffmpeg_decoder.c.

1510 {
1511 if (!decoder) {
1512 return;
1513 }
1514 atomic_ptr_store(&decoder->exit_callback_user_data, user_data);
1515 atomic_ptr_store(&decoder->should_exit_callback, (void *)should_exit_callback);
1516}
atomic_ptr_t exit_callback_user_data
User data for exit callback.
atomic_ptr_t should_exit_callback
Callback to check if app should exit.

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 <include/ascii-chat/media/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 1459 of file ffmpeg_decoder.c.

1459 {
1460 if (!decoder || decoder->video_stream_idx < 0) {
1461 return SET_ERRNO(ERROR_INVALID_PARAM, "Decoder has no initialized video prefetch buffers");
1462 }
1463
1464 if (!decoder->prefetch_image_a || !decoder->prefetch_image_b) {
1465 return SET_ERRNO(ERROR_INVALID_PARAM, "Decoder has no initialized video prefetch buffers");
1466 }
1467
1468 // Already running
1469 if (decoder->prefetch_thread_running) {
1470 return ASCIICHAT_OK;
1471 }
1472
1473 // Reset stop flag and create thread
1474 decoder->prefetch_should_stop = false;
1475 memset(&decoder->prefetch_failure, 0, sizeof(decoder->prefetch_failure));
1476 atomic_store(&decoder->prefetch_failed, false);
1477
1478 int thread_err = platform_thread_create(&decoder->prefetch_thread, "ffmpeg_prefetch",
1479 ffmpeg_decoder_prefetch_thread_func, decoder);
1480 if (thread_err != 0) {
1481 return SET_ERRNO(ERROR_THREAD, "Failed to create video prefetch thread");
1482 }
1483
1484 decoder->prefetch_thread_running = true;
1485 return ASCIICHAT_OK;
1486}
@ ERROR_THREAD

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_THREAD, ffmpeg_decoder_t::prefetch_failed, ffmpeg_decoder_t::prefetch_failure, 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 http_renderer_render_file(), and media_source_create_interruptible().

◆ ffmpeg_decoder_stop_prefetch()

void ffmpeg_decoder_stop_prefetch ( ffmpeg_decoder_t * decoder)

#include <include/ascii-chat/media/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 1491 of file ffmpeg_decoder.c.

1491 {
1492 if (!decoder || !decoder->prefetch_thread_running) {
1493 return;
1494 }
1495
1496 mutex_lock(&decoder->prefetch_mutex);
1497 decoder->prefetch_should_stop = true;
1498 cond_signal(&decoder->prefetch_cond);
1499 mutex_unlock(&decoder->prefetch_mutex);
1500 // Wait up to 2 seconds for thread to stop
1501 // The interrupt callback should cause av_read_frame to abort quickly
1502 if (thread_join_timeout(&decoder->prefetch_thread, NULL, 2000 * NS_PER_MS_INT) == ASCIICHAT_OK)
1503 decoder->prefetch_thread_running = false;
1504 // A timed-out worker remains owned and must be joined by decoder destruction.
1505 else
1506 log_warn("Video prefetch thread is still stopping");
1507}
#define NS_PER_MS_INT
Definition time.h:168

References ASCIICHAT_OK, log_warn, NS_PER_MS_INT, ffmpeg_decoder_t::prefetch_cond, ffmpeg_decoder_t::prefetch_mutex, 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 <include/ascii-chat/media/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 836 of file source.c.

836 {
837 if (!source) {
838 return true;
839 }
840
841 switch (source->type) {
844 return false; // Webcam never ends
845
848 if (!source->video_decoder) {
849 return true;
850 }
851 // If loop is enabled, we never truly reach end
852 if (source->loop_enabled && source->type == MEDIA_SOURCE_FILE) {
853 return false;
854 }
855 if (source->audio_only)
856 return source->audio_decoder && ffmpeg_decoder_at_end(source->audio_decoder);
857 return ffmpeg_decoder_at_end(source->video_decoder);
858
859 default:
860 return true;
861 }
862}

References ffmpeg_decoder_at_end(), MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, and source.

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 <include/ascii-chat/media/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 221 of file source.c.

221 {
222 return media_source_create_interruptible(type, path, NULL, NULL);
223}
media_source_t * media_source_create_interruptible(media_source_type_t type, const char *path, bool(*cancel)(void *), void *data)
Definition source.c:225

References media_source_create_interruptible().

◆ media_source_destroy()

void media_source_destroy ( media_source_t * source)

#include <include/ascii-chat/media/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 508 of file source.c.

508 {
509 ASCII_OTEL_SCOPE(MEDIA_SOURCE_DESTROY);
510 if (!source) {
511 return;
512 }
513
514 // Cleanup based on type
515 if (source->webcam_ctx) {
516 webcam_cleanup_context(source->webcam_ctx);
517 source->webcam_ctx = NULL;
518 }
519
520 // For YouTube (shared decoder), only destroy once
521 // For local files (separate decoders), destroy both
522 if (source->video_decoder) {
523 ffmpeg_decoder_destroy(source->video_decoder);
524 source->video_decoder = NULL;
525 }
526
527 if (source->audio_decoder) {
528 // Don't double-free if audio and video share the same decoder
529 if (!source->is_shared_decoder) {
530 ffmpeg_decoder_destroy(source->audio_decoder);
531 }
532 source->audio_decoder = NULL;
533 }
534
535 SAFE_FREE(source->file_path);
536 SAFE_FREE(source->original_youtube_url);
537
538 // Clean up test pattern frame
539 test_pattern_destroy(source->test_pattern);
540 source->test_pattern = NULL;
541
542 NAMED_UNREGISTER(&source->is_paused);
543
544 // Destroy mutexes
545 mutex_destroy(&source->decoder_mutex);
546
548}
void webcam_cleanup_context(webcam_context_t *ctx)
Clean up webcam context and release resources.
void test_pattern_destroy(test_pattern_t *p)

References ASCII_OTEL_SCOPE, ffmpeg_decoder_destroy(), NAMED_UNREGISTER, SAFE_FREE, source, test_pattern_destroy(), and webcam_cleanup_context().

Referenced by session_capture_create(), and session_capture_destroy().

◆ media_source_get_duration()

double media_source_get_duration ( media_source_t * source)

#include <include/ascii-chat/media/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 1003 of file source.c.

1003 {
1004 if (!source) {
1005 return -1.0;
1006 }
1007
1008 switch (source->type) {
1010 case MEDIA_SOURCE_TEST:
1011 return -1.0; // Infinite
1012
1013 case MEDIA_SOURCE_FILE:
1014 case MEDIA_SOURCE_STDIN:
1015 if (!source->video_decoder) {
1016 return -1.0;
1017 }
1018 return ffmpeg_decoder_get_duration(source->video_decoder);
1019
1020 default:
1021 return -1.0;
1022 }
1023}
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, and source.

◆ media_source_get_position()

double media_source_get_position ( media_source_t * source)

#include <include/ascii-chat/media/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 1025 of file source.c.

1025 {
1026 if (!source) {
1027 return -1.0;
1028 }
1029
1030 switch (source->type) {
1032 case MEDIA_SOURCE_TEST:
1033 return -1.0; // No position concept
1034
1035 case MEDIA_SOURCE_FILE:
1036 case MEDIA_SOURCE_STDIN:
1037 if (!source->video_decoder) {
1038 return -1.0;
1039 }
1040 return ffmpeg_decoder_get_position(source->audio_only ? source->audio_decoder : source->video_decoder);
1041
1042 default:
1043 return -1.0;
1044 }
1045}
double ffmpeg_decoder_get_position(ffmpeg_decoder_t *decoder)
Get current playback position in seconds.

References ffmpeg_decoder_get_position(), MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, and source.

◆ media_source_get_type()

media_source_type_t media_source_get_type ( media_source_t * source)

#include <include/ascii-chat/media/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 999 of file source.c.

999 {
1000 return source ? source->type : MEDIA_SOURCE_TEST;
1001}

References MEDIA_SOURCE_TEST, and source.

Referenced by session_capture_at_end(), session_capture_toggle_media(), and session_pipeline_create().

◆ media_source_get_video_fps()

double media_source_get_video_fps ( media_source_t * source)

#include <include/ascii-chat/media/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 the negotiated capture rate when available
TEST sources return 0.0
FILE/STDIN sources return the video stream's fps from FFmpeg

Definition at line 1047 of file source.c.

1047 {
1048 if (!source) {
1049 return 0.0;
1050 }
1051
1052 switch (source->type) {
1054 return source->webcam_ctx ? webcam_get_fps(source->webcam_ctx) : 0.0;
1055 case MEDIA_SOURCE_TEST:
1056 return 0.0;
1057
1058 case MEDIA_SOURCE_FILE:
1059 case MEDIA_SOURCE_STDIN:
1060 if (!source->video_decoder) {
1061 return 0.0;
1062 }
1063 return ffmpeg_decoder_get_video_fps(source->video_decoder);
1064
1065 default:
1066 return 0.0;
1067 }
1068}
double ffmpeg_decoder_get_video_fps(ffmpeg_decoder_t *decoder)
Get video frame rate.
double webcam_get_fps(webcam_context_t *ctx)

References ffmpeg_decoder_get_video_fps(), MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, source, and webcam_get_fps().

Referenced by session_capture_create().

◆ media_source_has_audio()

bool media_source_has_audio ( media_source_t * source)

#include <include/ascii-chat/media/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 801 of file source.c.

801 {
802 if (!source) {
803 return false;
804 }
805
806 switch (source->type) {
809 return false; // Webcam/test don't have audio
810
813 return source->audio_decoder && ffmpeg_decoder_has_audio(source->audio_decoder);
814
815 default:
816 return false;
817 }
818}
bool ffmpeg_decoder_has_audio(ffmpeg_decoder_t *decoder)
Check if decoder has audio stream.

References ffmpeg_decoder_has_audio(), MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, and source.

Referenced by audio_connection_init(), audio_start_duplex(), session_audio_start_duplex(), session_capture_create(), and session_participant_start_audio().

◆ media_source_has_video()

bool media_source_has_video ( media_source_t * source)

#include <include/ascii-chat/media/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 699 of file source.c.

699 {
700 if (!source) {
701 return false;
702 }
703
704 switch (source->type) {
707 return true; // Webcam/test always has video
708
711 return source->audio_only || (source->video_decoder && ffmpeg_decoder_has_video(source->video_decoder));
712
713 default:
714 return false;
715 }
716}
bool ffmpeg_decoder_has_video(ffmpeg_decoder_t *decoder)
Check if decoder has video stream.

References ffmpeg_decoder_has_video(), MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, and source.

◆ media_source_is_paused()

bool media_source_is_paused ( media_source_t * source)

#include <include/ascii-chat/media/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 1088 of file source.c.

1088 {
1089 if (!source) {
1090 return false;
1091 }
1092 return atomic_load_bool(&source->is_paused);
1093}

References source.

◆ media_source_pause()

void media_source_pause ( media_source_t * source)

#include <include/ascii-chat/media/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 1074 of file source.c.

1074 {
1075 if (!source) {
1076 return;
1077 }
1078 atomic_store_bool(&source->is_paused, true);
1079}

References source.

◆ media_source_read_audio()

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

#include <include/ascii-chat/media/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, interleaved stereo, 48kHz sample rate (compatible with Opus encoding).

Audio format:

  • Format: 32-bit float
  • Channels: Stereo (2 channels); sample counts include both channels
  • 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 stereo automatically

Definition at line 722 of file source.c.

722 {
723 ASCII_OTEL_SCOPE(MEDIA_SOURCE_READ_AUDIO);
724 if (!source || !buffer || num_samples == 0) {
725 return 0;
726 }
727
728 static uint64_t call_count = 0;
729 call_count++;
730 if (call_count <= 5 || call_count % 1000 == 0) {
731 log_debug("#%lu: source_type=%d num_samples=%zu", call_count, source->type, num_samples);
732 }
733
734 // Check pause state (thread-safe)
735 bool is_paused = atomic_load_bool(&source->is_paused);
736
737 // Return silence immediately if paused (maintaining position)
738 if (is_paused) {
739 memset(buffer, 0, num_samples * sizeof(float));
740 return num_samples;
741 }
742
743 switch (source->type) {
746 // Webcam/test pattern don't provide audio
747 return 0;
748
750 case MEDIA_SOURCE_STDIN: {
751 if (!source->audio_decoder) {
752 log_warn_every(US_PER_SEC_INT, "audio_decoder is NULL!");
753 return 0;
754 }
755
756 // Lock shared decoder if YouTube URL (protect against concurrent video thread access)
757 if (source->is_shared_decoder) {
758 mutex_lock(&source->decoder_mutex);
759 }
760
761 double audio_pos_before_read = ffmpeg_decoder_get_position(source->audio_decoder);
762 size_t samples_read = ffmpeg_decoder_read_audio_samples(source->audio_decoder, buffer, num_samples);
763 double audio_pos_after_read = ffmpeg_decoder_get_position(source->audio_decoder);
764
765 static double last_audio_pos = 0;
766 if (audio_pos_after_read >= 0 && last_audio_pos >= 0 && audio_pos_after_read < last_audio_pos) {
767 log_debug_every(US_PER_SEC_INT, "Audio position moved backward: %.2f → %.2f (LOOPING!)", last_audio_pos,
768 audio_pos_after_read);
769 }
770 if (audio_pos_after_read >= 0) {
771 last_audio_pos = audio_pos_after_read;
772 }
773
774 log_debug_every(US_PER_SEC_INT, "Audio: read %zu samples, pos %.2f → %.2f", samples_read,
775 audio_pos_before_read, audio_pos_after_read);
776
777 // Handle EOF with loop
778 if (samples_read == 0 && ffmpeg_decoder_at_end(source->audio_decoder)) {
779 if (source->loop_enabled && source->type == MEDIA_SOURCE_FILE) {
780 log_debug("End of file reached (audio), rewinding for loop");
782 // Try reading again after rewind
783 samples_read = ffmpeg_decoder_read_audio_samples(source->audio_decoder, buffer, num_samples);
784 }
785 }
786 }
787
788 // Unlock shared decoder
789 if (source->is_shared_decoder) {
790 mutex_unlock(&source->decoder_mutex);
791 }
792
793 return samples_read;
794 }
795
796 default:
797 return 0;
798 }
799}
asciichat_error_t media_source_rewind(media_source_t *source)
Rewind media source to beginning.
Definition source.c:864
size_t ffmpeg_decoder_read_audio_samples(ffmpeg_decoder_t *decoder, float *buffer, size_t num_samples)
Decode audio samples.

References ASCII_OTEL_SCOPE, ASCIICHAT_OK, ffmpeg_decoder_at_end(), ffmpeg_decoder_get_position(), ffmpeg_decoder_read_audio_samples(), log_debug, log_debug_every, log_warn_every, MEDIA_SOURCE_FILE, media_source_rewind(), MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, source, and US_PER_SEC_INT.

◆ media_source_read_video()

image_t * media_source_read_video ( media_source_t * source)

#include <include/ascii-chat/media/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 592 of file source.c.

592 {
593 ASCII_OTEL_SCOPE(MEDIA_SOURCE_READ_VIDEO);
594 if (!source) {
595 return NULL;
596 }
597
598 // Check pause state (thread-safe)
599 bool is_paused = atomic_load_bool(&source->is_paused);
600
601 // Return NULL immediately if paused (maintaining position)
602 if (is_paused) {
603 return NULL;
604 }
605
606 if (source->audio_only && media_source_at_end(source))
607 return NULL;
608 // Audio visualizations replace the image at display time and need only a frame clock.
609 if ((source->audio_only && GET_OPTION(test_pattern)) ||
610 ((source->audio_only || source->type == MEDIA_SOURCE_WEBCAM) && (GET_OPTION(waveform) || GET_OPTION(fft))))
611 return read_test_pattern(source);
612 if ((source->audio_only || source->type == MEDIA_SOURCE_WEBCAM) && !source->webcam_ctx) {
614 return NULL;
615 }
616 if (source->webcam_ctx)
617 return webcam_read_context(source->webcam_ctx);
618
619 switch (source->type) {
621 // Read from webcam
622 if (source->webcam_ctx) {
623 return webcam_read_context(source->webcam_ctx);
624 }
625 return NULL;
626
627 case MEDIA_SOURCE_TEST: {
628 return read_test_pattern(source);
629 }
630
632 case MEDIA_SOURCE_STDIN: {
633 if (!source->video_decoder) {
634 return NULL;
635 }
636
637 // Lock shared decoder if YouTube URL (protect against concurrent audio thread access)
638 if (source->is_shared_decoder) {
639 mutex_lock(&source->decoder_mutex);
640 }
641
642 uint64_t frame_read_start_ns = time_get_ns();
643 image_t *frame = ffmpeg_decoder_read_video_frame(source->video_decoder);
644 uint64_t frame_read_ns = time_elapsed_ns(frame_read_start_ns, time_get_ns());
645
646 // Track frame reading statistics for FPS diagnosis
647 static uint64_t total_attempts = 0;
648 static uint64_t successful_frames = 0;
649 static uint64_t null_frame_count = 0;
650 static uint64_t total_read_time_ns = 0;
651 static uint64_t max_read_time_ns = 0;
652
653 total_attempts++;
654 if (frame) {
655 successful_frames++;
656 total_read_time_ns += frame_read_ns;
657 if (frame_read_ns > max_read_time_ns) {
658 max_read_time_ns = frame_read_ns;
659 }
660
661 // Log statistics every 30 successful frames
662 if (successful_frames % 30 == 0) {
663 double avg_read_ms = (double)total_read_time_ns / (double)successful_frames / NS_PER_MS;
664 double max_read_ms = (double)max_read_time_ns / NS_PER_MS;
665 double null_rate = (double)null_frame_count * 100.0 / (double)total_attempts;
667 "FRAME_STATS[%lu]: avg_read=%.2f ms, max_read=%.2f ms, null_rate=%.1f%% "
668 "(%lu null/%lu attempts)",
669 successful_frames, avg_read_ms, max_read_ms, null_rate, null_frame_count, total_attempts);
670 }
671 } else {
672 null_frame_count++;
673 }
674
675 // Handle EOF with loop
676 if (!frame && ffmpeg_decoder_at_end(source->video_decoder)) {
677 if (source->loop_enabled && source->type == MEDIA_SOURCE_FILE) {
678 log_debug("End of file reached, rewinding for loop");
680 // Try reading again after rewind
681 frame = ffmpeg_decoder_read_video_frame(source->video_decoder);
682 }
683 }
684 }
685
686 // Unlock shared decoder
687 if (source->is_shared_decoder) {
688 mutex_unlock(&source->decoder_mutex);
689 }
690
691 return frame;
692 }
693
694 default:
695 return NULL;
696 }
697}
image_t * ffmpeg_decoder_read_video_frame(ffmpeg_decoder_t *decoder)
#define NS_PER_MS
Definition time.h:159
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition time.c:123
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:571
test pattern is intentionally a small, concrete record. Fields such as image, rgba give callers the i...

References ASCII_OTEL_SCOPE, ASCIICHAT_OK, ffmpeg_decoder_at_end(), ffmpeg_decoder_read_video_frame(), GET_OPTION, log_debug, log_debug_every, media_source_at_end(), MEDIA_SOURCE_FILE, media_source_rewind(), media_source_start_video(), MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, NS_PER_MS, source, time_elapsed_ns(), US_PER_SEC_INT, and webcam_read_context().

◆ media_source_resume()

void media_source_resume ( media_source_t * source)

#include <include/ascii-chat/media/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 1081 of file source.c.

1081 {
1082 if (!source) {
1083 return;
1084 }
1085 atomic_store_bool(&source->is_paused, false);
1086}

References source.

◆ media_source_rewind()

asciichat_error_t media_source_rewind ( media_source_t * source)

#include <include/ascii-chat/media/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 864 of file source.c.

864 {
865 ASCII_OTEL_SCOPE(MEDIA_SOURCE_REWIND);
866 if (!source) {
867 return ERROR_INVALID_PARAM;
868 }
869
870 switch (source->type) {
873 return ASCIICHAT_OK; // No-op for webcam
874
876 if (!source->video_decoder && !source->audio_decoder) {
877 return ERROR_INVALID_PARAM;
878 }
879
880 // Lock shared decoder if YouTube URL (protect against concurrent thread access)
881 if (source->is_shared_decoder) {
882 mutex_lock(&source->decoder_mutex);
883 }
884
885 // Rewind video decoder
886 asciichat_error_t video_result =
887 source->video_decoder ? ffmpeg_decoder_rewind(source->video_decoder) : ASCIICHAT_OK;
888 if (video_result != ASCIICHAT_OK) {
889 if (source->is_shared_decoder) {
890 mutex_unlock(&source->decoder_mutex);
891 }
892 return video_result;
893 }
894
895 // For YouTube (shared decoder), don't rewind audio separately
896 // For local files (separate decoders), rewind audio too
898 if (!source->is_shared_decoder && source->audio_decoder) {
899 result = ffmpeg_decoder_rewind(source->audio_decoder);
900 }
901
902 // Unlock shared decoder
903 if (source->is_shared_decoder) {
904 mutex_unlock(&source->decoder_mutex);
905 }
906
907 return result;
908
910 return ERROR_NOT_SUPPORTED; // Cannot seek stdin
911
912 default:
913 return ERROR_INVALID_PARAM;
914 }
915}
asciichat_error_t ffmpeg_decoder_rewind(ffmpeg_decoder_t *decoder)
Seek to beginning of media.

References ASCII_OTEL_SCOPE, ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_NOT_SUPPORTED, ffmpeg_decoder_rewind(), MEDIA_SOURCE_FILE, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, and source.

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 <include/ascii-chat/media/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 917 of file source.c.

917 {
918 ASCII_OTEL_SCOPE(MEDIA_SOURCE_SEEK);
919 if (!source) {
920 SET_ERRNO(ERROR_INVALID_PARAM, "Media source is NULL");
921 return ERROR_INVALID_PARAM;
922 }
923
924 if (timestamp_sec < 0.0) {
925 SET_ERRNO(ERROR_INVALID_PARAM, "Timestamp must be >= 0.0");
926 return ERROR_INVALID_PARAM;
927 }
928
929 // WEBCAM and TEST sources: no-op (always return OK)
930 if (source->type == MEDIA_SOURCE_WEBCAM || source->type == MEDIA_SOURCE_TEST) {
931 return ASCIICHAT_OK;
932 }
933
934 // STDIN: not supported
935 if (source->type == MEDIA_SOURCE_STDIN) {
936 SET_ERRNO(ERROR_NOT_SUPPORTED, "Cannot seek stdin");
937 return ERROR_NOT_SUPPORTED;
938 }
939
941
942 // Clear audio playback buffer BEFORE seeking to prevent old audio from being queued
943 // This ensures fresh audio starts playing immediately after seek completes
944 if (source->audio_ctx) {
945 audio_context_t *audio_ctx = (audio_context_t *)source->audio_ctx;
946 if (audio_ctx->playback_buffer) {
947 audio_ring_buffer_clear(audio_ctx->playback_buffer);
948 }
949 }
950
951 // The seeking_in_progress flag in decoders blocks prefetch thread
952 // seeking_in_progress coordinates with audio callback via condition variable
953
954 uint64_t seek_start_ns = time_get_ns();
955 if (source->video_decoder) {
956 double video_pos_before = ffmpeg_decoder_get_position(source->video_decoder);
957 asciichat_error_t video_err = ffmpeg_decoder_seek_to_timestamp(source->video_decoder, timestamp_sec);
958 double video_pos_after = ffmpeg_decoder_get_position(source->video_decoder);
959 uint64_t video_seek_ns = time_elapsed_ns(seek_start_ns, time_get_ns());
960 char video_seek_str[TIME_DURATION_BUFFER_SIZE];
961 time_pretty(video_seek_ns, -1, video_seek_str, sizeof(video_seek_str));
962 if (video_err != ASCIICHAT_OK) {
963 log_warn("Video seek to %.2f failed %s (took %s)", timestamp_sec, asciichat_error_format(NULL, video_err),
964 video_seek_str);
965 result = video_err;
966 } else {
967 log_debug("Video SEEK: %.2f → %.2f sec (target %.2f, took %s)", video_pos_before, video_pos_after,
968 timestamp_sec, video_seek_str);
969 }
970 }
971
972 // Seek audio decoder (if separate from video)
973 if (source->audio_decoder && !source->is_shared_decoder) {
974 log_debug("=== Starting audio seek to %.2f sec ===", timestamp_sec);
975 uint64_t audio_seek_start_ns = time_get_ns();
976 double audio_pos_before = ffmpeg_decoder_get_position(source->audio_decoder);
977 log_debug("Audio position before seek: %.2f", audio_pos_before);
978 asciichat_error_t audio_err = ffmpeg_decoder_seek_to_timestamp(source->audio_decoder, timestamp_sec);
979 double audio_pos_after = ffmpeg_decoder_get_position(source->audio_decoder);
980 uint64_t audio_seek_ns = time_elapsed_ns(audio_seek_start_ns, time_get_ns());
981 log_debug("Audio position after seek: %.2f", audio_pos_after);
982 char audio_seek_str[TIME_DURATION_BUFFER_SIZE];
983 time_pretty(audio_seek_ns, -1, audio_seek_str, sizeof(audio_seek_str));
984 if (audio_err != ASCIICHAT_OK) {
985 log_warn("Audio seek to %.2f failed %s (took %s)", timestamp_sec, asciichat_error_format(NULL, audio_err),
986 audio_seek_str);
987 result = audio_err;
988 } else {
989 log_debug("Audio SEEK COMPLETE: %.2f → %.2f sec (target %.2f, took %s)", audio_pos_before, audio_pos_after,
990 timestamp_sec, audio_seek_str);
991 }
992 }
993
994 // Prefetch thread automatically resumes when seeking_in_progress is cleared and signaled
995
996 return result;
997}
const char * asciichat_error_format(const char *message, asciichat_error_t code)
void audio_ring_buffer_clear(audio_ring_buffer_t *rb)
Clear all audio samples from ring buffer.
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as a compact duration with configurable sub-minute precision.
Definition time.c:374
#define TIME_DURATION_BUFFER_SIZE
Definition time.h:40
When code needs to work with audio device info, it keeps index, max_input_channels,...

References ASCII_OTEL_SCOPE, asciichat_error_format(), ASCIICHAT_OK, audio_ring_buffer_clear(), ERROR_INVALID_PARAM, ERROR_NOT_SUPPORTED, ffmpeg_decoder_get_position(), ffmpeg_decoder_seek_to_timestamp(), log_debug, log_warn, MEDIA_SOURCE_STDIN, MEDIA_SOURCE_TEST, MEDIA_SOURCE_WEBCAM, audio_context_t::playback_buffer, SET_ERRNO, source, TIME_DURATION_BUFFER_SIZE, time_elapsed_ns(), and time_pretty().

Referenced by session_capture_create().

◆ media_source_set_audio_context()

void media_source_set_audio_context ( media_source_t * source,
void * audio_ctx )

#include <include/ascii-chat/media/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 1104 of file source.c.

1104 {
1105 if (source) {
1106 source->audio_ctx = audio_ctx;
1107 }
1108}

References source.

◆ media_source_set_exit_callback()

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

#include <include/ascii-chat/media/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 1110 of file source.c.

1110 {
1111 if (!source) {
1112 return;
1113 }
1114
1115 // Set exit callback on video decoder if present
1116 if (source->video_decoder) {
1117 ffmpeg_decoder_set_exit_callback(source->video_decoder, should_exit_callback, user_data);
1118 }
1119
1120 // Set exit callback on audio decoder if present (only if different from video decoder)
1121 if (source->audio_decoder && source->audio_decoder != source->video_decoder) {
1122 ffmpeg_decoder_set_exit_callback(source->audio_decoder, should_exit_callback, user_data);
1123 }
1124}
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 ffmpeg_decoder_set_exit_callback(), and source.

Referenced by session_capture_create().

◆ media_source_set_loop()

void media_source_set_loop ( media_source_t * source,
bool loop )

#include <include/ascii-chat/media/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 824 of file source.c.

824 {
825 if (!source) {
826 return;
827 }
828
829 source->loop_enabled = loop;
830
831 if (loop && source->type == MEDIA_SOURCE_STDIN) {
832 log_warn("Loop mode not supported for stdin input (cannot seek)");
833 }
834}

References log_warn, MEDIA_SOURCE_STDIN, and source.

Referenced by session_capture_create().

◆ media_source_toggle_pause()

void media_source_toggle_pause ( media_source_t * source)

#include <include/ascii-chat/media/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 1095 of file source.c.

1095 {
1096 if (!source) {
1097 return;
1098 }
1099 bool paused = atomic_load_bool(&source->is_paused);
1100 while (!atomic_cas_bool(&source->is_paused, &paused, !paused)) {
1101 }
1102}

References source.

◆ yt_dlp_extract_stream_urls()

asciichat_error_t yt_dlp_extract_stream_urls ( const char * url,
const char * yt_dlp_options,
char * output_url,
size_t output_size,
char * audio_url,
size_t audio_size )

#include <include/ascii-chat/media/yt_dlp.h>

For yt dlp request options, this is the live record that ties proxy_specified together. The owning code creates it, updates it while work is active, and releases its handles and pointed-to storage before freeing the record.

Extract stream URL using yt-dlp with optional parameters

Prefers a combined stream, falling back to separate video and audio streams. 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" Quotes group arguments; no shell expansion is performed. Pass NULL or "" for no extra options.
output_urlOutput buffer for extracted stream URL
output_sizeSize of output buffer (recommend 8192+ bytes)
audio_urlOutput buffer for a separate audio URL, or an empty string when the first URL contains both streams
audio_sizeSize of the audio URL buffer
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

Definition at line 552 of file yt_dlp.c.

553 {
554 return yt_dlp_extract_stream_urls_interruptible(url, yt_dlp_options, output_url, output_size, audio_url, audio_size,
555 NULL, NULL);
556}
asciichat_error_t yt_dlp_extract_stream_urls_interruptible(const char *url, const char *yt_dlp_options, char *output_url, size_t output_size, char *audio_url, size_t audio_size, bool(*cancel)(void *), void *data)
Definition yt_dlp.c:544

References yt_dlp_extract_stream_urls_interruptible().

Referenced by yt_dlp_extract_stream_url().

◆ yt_dlp_is_available()

bool yt_dlp_is_available ( void )

#include <include/ascii-chat/media/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 178 of file yt_dlp.c.

178 {
179 ASCII_OTEL_SCOPE(YTDLP_AVAILABLE);
180 ASCII_OTEL_ATTR_STRING("media.extractor", "yt-dlp");
181 const char *argv[] = {"yt-dlp", "--version", NULL};
182 int ret = 0;
183 LOG_IO("yt-dlp", { ret = platform_execute_subprocess("yt-dlp", argv, NULL, 0, NULL, NULL); });
184 return (ret == 0);
185}
int platform_execute_subprocess(const char *executable, const char **argv, char *output_buffer, size_t output_size, bool(*cancel)(void *), void *data)
Execute a subprocess and optionally capture its output.
#define ASCII_OTEL_ATTR_STRING(key, value)
Definition otel.h:66

References ASCII_OTEL_ATTR_STRING, ASCII_OTEL_SCOPE, LOG_IO, and platform_execute_subprocess().