ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
ffmpeg_encoder.c File Reference

FFmpeg video/image file encoder for render-file output. More...

Go to the source code of this file.

Data Structures

struct  ffmpeg_encoder_s
 This is the handoff point for ffmpeg encoder. Callers fill codec_ctx, stream, frame, frame_encoded before passing it on; consumers read the values during the operation and return ownership only where the member contract explicitly says they do. More...

Macros

#define MEDIA_FFMPEG_ENCODER_MESSAGE_BUFFER_SIZE   2048
#define FFMPEG_STDOUT_AVIO_BUFFER_SIZE   32768

Functions

asciichat_error_t ffmpeg_encoder_create (const char *output_path, int width_px, int height_px, int fps, ffmpeg_encoder_t **out)
asciichat_error_t ffmpeg_encoder_create_scaled (const char *output_path, int source_width, int source_height, int width_px, int height_px, int fps, ffmpeg_encoder_t **out)
asciichat_error_t ffmpeg_encoder_write_frame (ffmpeg_encoder_t *enc, const uint8_t *rgb, int pitch, uint64_t captured_ns)
asciichat_error_t ffmpeg_encoder_write_audio (ffmpeg_encoder_t *enc, const float *samples, int num_samples)
void ffmpeg_encoder_set_snapshot_actual_duration (ffmpeg_encoder_t *enc, double actual_duration_sec)
asciichat_error_t ffmpeg_encoder_destroy (ffmpeg_encoder_t *enc)
void ffmpeg_encoder_set_live_timing (ffmpeg_encoder_t *enc)

Detailed Description

FFmpeg video/image file encoder for render-file output.

Definition in file ffmpeg_encoder.c.

Macro Definition Documentation

◆ FFMPEG_STDOUT_AVIO_BUFFER_SIZE

#define FFMPEG_STDOUT_AVIO_BUFFER_SIZE   32768

Definition at line 11 of file ffmpeg_encoder.c.

◆ MEDIA_FFMPEG_ENCODER_MESSAGE_BUFFER_SIZE

#define MEDIA_FFMPEG_ENCODER_MESSAGE_BUFFER_SIZE   2048

Definition at line 8 of file ffmpeg_encoder.c.

Function Documentation

◆ ffmpeg_encoder_create()

asciichat_error_t ffmpeg_encoder_create ( const char * output_path,
int width_px,
int height_px,
int fps,
ffmpeg_encoder_t ** out )

Create and initialize a video/image encoder.

The output format and codec are automatically determined from the file extension. See Supported Formats & Codecs for supported formats.

Bitrate: Automatically calculated from resolution (~1 Mbps per megapixel, clamped to 500-5000 kbps).

Color Space: Input is expected in RGBA format. Internal conversion to the appropriate output pixel format (YUV420P for video, RGB24 for PNG, YUVJ420P for JPEG) is handled automatically.

Parameters
output_pathPath to the output file (extension determines format and codec)
width_pxVideo width in pixels (must be > 0)
height_pxVideo height in pixels (must be > 0)
fpsFrames per second (must be > 0)
[out]outPointer to encoder handle (set on success)
Returns
ASCIICHAT_OK on success, error code otherwise
Note
The encoder acquires resources (file handles, memory buffers) that must be released with ffmpeg_encoder_destroy(). Failure to do so may leak file handles and memory.
See also
ffmpeg_encoder_write_frame, ffmpeg_encoder_destroy, ffmpeg_encoder_formats

Definition at line 737 of file ffmpeg_encoder.c.

738 {
739 return ffmpeg_encoder_create_scaled(output_path, width_px, height_px, width_px, height_px, fps, out);
740}
asciichat_error_t ffmpeg_encoder_create_scaled(const char *output_path, int source_width, int source_height, int width_px, int height_px, int fps, ffmpeg_encoder_t **out)
static bool size_t gotel_query_t * out
Definition queries.h:8

References ffmpeg_encoder_create_scaled(), and out.

◆ ffmpeg_encoder_create_scaled()

asciichat_error_t ffmpeg_encoder_create_scaled ( const char * output_path,
int source_width,
int source_height,
int width_px,
int height_px,
int fps,
ffmpeg_encoder_t ** out )

Create an encoder that scales RGBA input to the requested video dimensions.

Definition at line 742 of file ffmpeg_encoder.c.

743 {
744 log_info("output=%s, %dx%d @ %dfps", output_path, width_px, height_px, fps);
745
746 if (!output_path || !out || source_width <= 0 || source_height <= 0 || width_px <= 0 || height_px <= 0)
747 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid parameters");
748 if (strcmp(output_path, "pipe:") == 0)
749 output_path = "-";
750 // Codec workers can log after LOG_IO restores the process descriptors.
751 // The session logger routes diagnostics to stderr when binary stdout is active.
752 if (strcmp(output_path, "-") == 0)
753 av_log_set_callback(encoder_log_callback);
754
755 // Use default FPS if not specified (0 from config means "use default")
756 if (fps <= 0) {
757 fps = 60;
758 log_debug("fps was 0, using default 60");
759 }
760
761 ffmpeg_encoder_t *enc = SAFE_CALLOC(1, sizeof(*enc), ffmpeg_encoder_t *);
762 enc->width_px = width_px;
763 enc->height_px = height_px;
764 enc->source_width = source_width;
765 enc->source_height = source_height;
766
767 encoder_init_snapshot_timing(enc, fps);
768
769 enc->is_stdout_pipe = (output_path && strcmp(output_path, "-") == 0) ? 1 : 0;
770
771 // For codec/format detection, use output_path unless it's "-" (stdout)
772 // For stdout, use "fake.mp4" to default to MP4 format
773 const char *detection_path = output_path;
774 if (output_path && strcmp(output_path, "-") == 0) {
775 detection_path = "fake.mp4"; // Fake path for format detection when piping to stdout
776 log_debug("Using MP4 format for stdout piping");
777 }
778
779 const char *codec_name = NULL, *format_name = NULL;
780 get_codec_from_extension(detection_path, &codec_name, &format_name, &enc->is_image, &enc->target_pix_fmt);
781
782 log_debug("%s (%s, %dx%d @ %dfps, %s)", output_path, codec_name, width_px, height_px, fps,
783 enc->is_image ? "image" : "video");
784
785 // Allocate output media context
786 int ret = avformat_alloc_output_context2(&enc->fmt_ctx, NULL, format_name, output_path);
787 if (ret < 0) {
788 SAFE_FREE(enc);
789 return SET_ERRNO(ERROR_INIT, "ffmpeg: avformat_alloc_output_context2 failed");
790 }
791
792 asciichat_error_t result = encoder_init_video_stream(enc, codec_name);
793 if (result != ASCIICHAT_OK)
794 return result;
795
796 AVDictionary *opts = encoder_create_muxer_options(enc, output_path);
797
798 result = encoder_init_video_frames(enc);
799 if (result != ASCIICHAT_OK)
800 return result;
801
802 // Initialize audio stream if supported
803 encoder_init_audio_stream(enc, output_path);
804
805 result = encoder_open_output(enc, output_path, opts);
806 if (result != ASCIICHAT_OK)
807 return result;
808
809 encoder_set_snapshot_frame_duration(enc);
810
811 enc->frame_count = 0;
812 enc->video_pts = 0;
813 enc->snapshot_actual_duration = 0.0;
814 enc->snapshot_elapsed_ns = 0;
815 enc->first_frame_captured_ns = 0; // Initialize first frame timestamp
816 *out = enc;
817
818 /* Register encoder with named registry */
819 NAMED_REGISTER_FFMPEG_ENCODER(enc, output_path, NULL);
820
821 log_info("SUCCESS: encoder initialized, frame_count=0, fps=%d, dims=%dx%d", enc->fps, enc->width_px, enc->height_px);
822 return ASCIICHAT_OK;
823}
#define SET_ERRNO(code,...)
struct ffmpeg_encoder_s ffmpeg_encoder_t
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
#define NAMED_REGISTER_FFMPEG_ENCODER(encoder, name, parent_ptr)
Register an FFmpeg encoder with automatic format specifier.
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INIT
Definition error_codes.h:56
@ ERROR_INVALID_PARAM
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
uint64_t first_frame_captured_ns
enum AVPixelFormat target_pix_fmt
uint64_t snapshot_elapsed_ns
AVFormatContext * fmt_ctx
double snapshot_actual_duration

References ASCIICHAT_OK, ERROR_INIT, ERROR_INVALID_PARAM, ffmpeg_encoder_s::first_frame_captured_ns, ffmpeg_encoder_s::fmt_ctx, ffmpeg_encoder_s::fps, ffmpeg_encoder_s::frame_count, ffmpeg_encoder_s::height_px, ffmpeg_encoder_s::is_image, ffmpeg_encoder_s::is_stdout_pipe, log_debug, log_info, NAMED_REGISTER_FFMPEG_ENCODER, out, SAFE_CALLOC, SAFE_FREE, SET_ERRNO, ffmpeg_encoder_s::snapshot_actual_duration, ffmpeg_encoder_s::snapshot_elapsed_ns, ffmpeg_encoder_s::source_height, ffmpeg_encoder_s::source_width, ffmpeg_encoder_s::target_pix_fmt, ffmpeg_encoder_s::video_pts, and ffmpeg_encoder_s::width_px.

Referenced by ffmpeg_encoder_create(), and render_file_create().

◆ ffmpeg_encoder_destroy()

asciichat_error_t ffmpeg_encoder_destroy ( ffmpeg_encoder_t * enc)

Close and finalize the output file, releasing all encoder resources.

This function flushes any pending frames in the encoder, writes the file trailer, and releases all allocated resources (memory buffers, file handles, codecs).

Critical: This MUST be called to properly finalize the output file. Without this call, the file may be incomplete or unplayable.

Parameters
encEncoder handle (created with ffmpeg_encoder_create())
Returns
ASCIICHAT_OK on success (currently always succeeds)
Note
Safe to call with NULL pointer (returns ASCIICHAT_OK).
After this call, enc must not be used again.
The output file is NOT guaranteed to be complete until this function returns.
See also
ffmpeg_encoder_create

Definition at line 1352 of file ffmpeg_encoder.c.

1352 {
1353 if (!enc)
1354 return ASCIICHAT_OK;
1355
1357 NAMED_UNREGISTER(enc);
1358
1359 encoder_log_snapshot_duration(enc);
1360
1361 result = encoder_flush_video(enc);
1362
1363 asciichat_error_t audio_result = encoder_flush_audio(enc);
1364 if (audio_result != ASCIICHAT_OK)
1365 result = audio_result;
1366
1367 encoder_set_stream_duration(enc);
1368
1369 asciichat_error_t output_result = encoder_finalize_output(enc);
1370 if (output_result != ASCIICHAT_OK)
1371 result = output_result;
1372
1373 encoder_free(enc);
1374 return result;
1375}
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.

References ASCIICHAT_OK, and NAMED_UNREGISTER.

Referenced by render_file_create(), and render_file_destroy().

◆ ffmpeg_encoder_set_live_timing()

void ffmpeg_encoder_set_live_timing ( ffmpeg_encoder_t * enc)

Definition at line 1377 of file ffmpeg_encoder.c.

1377 {
1378 if (enc)
1379 enc->live_timing = true;
1380}

References ffmpeg_encoder_s::live_timing.

Referenced by render_file_set_live_timing().

◆ ffmpeg_encoder_set_snapshot_actual_duration()

void ffmpeg_encoder_set_snapshot_actual_duration ( ffmpeg_encoder_t * enc,
double actual_duration_sec )

Set the actual wall-clock duration for snapshot mode frame timing.

In snapshot mode, frames are captured over a wall-clock duration (e.g., 3 seconds of real time). This function should be called after all frames are captured to provide the actual elapsed time, allowing the encoder to calculate correct frame durations so the output video duration matches the actual capture time.

Parameters
encEncoder handle (created with ffmpeg_encoder_create())
actual_duration_secActual wall-clock duration in seconds (e.g., 3.5 for 3.5 seconds)
Note
Only used in snapshot mode. In normal mode, this is ignored.
Should be called before ffmpeg_encoder_destroy() for the adjustment to take effect.
If not called, frame durations are calculated from FPS, which may not match actual wall-clock capture time.
See also
ffmpeg_encoder_create, ffmpeg_encoder_destroy

Definition at line 1089 of file ffmpeg_encoder.c.

1089 {
1090 if (!enc)
1091 return;
1092 enc->snapshot_actual_duration = actual_duration_sec;
1093
1094 // If we already have frame count, we can calculate the adjusted packet duration now
1095 // This will be used for any remaining frames and during flush
1096 if (enc->frame_count > 0 && actual_duration_sec > 0) {
1097 // Calculate the packet duration that would make all frames fit in actual_duration_sec
1098 // packet_duration_units = (actual_duration_sec * time_base.den) / frame_count
1099 // This is only a starting point - the actual duration might be different if more frames are captured
1100 log_debug("%.3f sec with %d frames so far", actual_duration_sec, enc->frame_count);
1101 } else {
1102 log_info("Setting duration to %.3f seconds (already encoded %d frames)", actual_duration_sec, enc->frame_count);
1103 }
1104}

References ffmpeg_encoder_s::frame_count, log_debug, log_info, and ffmpeg_encoder_s::snapshot_actual_duration.

Referenced by render_file_set_snapshot_actual_duration().

◆ ffmpeg_encoder_write_audio()

asciichat_error_t ffmpeg_encoder_write_audio ( ffmpeg_encoder_t * enc,
const float * samples,
int num_samples )

Write audio samples to the output file.

Encodes and writes audio samples to the output file. Audio samples must be in 48kHz mono float32 format. The function internally buffers and accumulates samples into codec frames before encoding.

Audio Format Requirements:

  • Sample rate: 48000 Hz (48 kHz)
  • Channels: Mono (1 channel)
  • Format: 32-bit IEEE float (AV_SAMPLE_FMT_FLT)

Notes:

  • Audio is only written if the output format supports audio (MP4, WebM, AVI).
  • Image formats (PNG, JPEG, GIF) ignore audio writes.
  • Samples are buffered internally and encoded in chunks as needed.
  • Timing is automatically synchronized with video using PTS (presentation timestamp).
Parameters
encEncoder handle (created with ffmpeg_encoder_create())
samplesPointer to audio samples in float32 format (not owned, used immediately)
num_samplesNumber of samples to write (must be > 0)
Returns
ASCIICHAT_OK on success, error code otherwise
Note
Input sample data is NOT owned by the encoder. The caller must ensure the buffer remains valid throughout the call.
Safe to call on encoders without audio streams (no-op).
See also
ffmpeg_encoder_create, ffmpeg_encoder_write_frame

Definition at line 1025 of file ffmpeg_encoder.c.

1025 {
1026 if (!enc || !samples || num_samples <= 0)
1027 return ASCIICHAT_OK; // Silently ignore invalid calls
1028
1029 // No audio stream for this format
1030 if (!enc->has_audio_stream)
1031 return ASCIICHAT_OK;
1032
1033 // Accumulate samples into partial buffer
1034 int samples_to_process = num_samples;
1035 int samples_offset = 0;
1036
1037 while (samples_to_process > 0) {
1038 // How many samples can we add to the partial buffer?
1039 int space_in_buffer = enc->audio_frame_size * 2 - enc->audio_partial_len;
1040 int samples_to_copy = (samples_to_process < space_in_buffer) ? samples_to_process : space_in_buffer;
1041
1042 // Copy samples to partial buffer
1043 memcpy(&enc->audio_partial_buf[enc->audio_partial_len], &samples[samples_offset], samples_to_copy * sizeof(float));
1044 enc->audio_partial_len += samples_to_copy;
1045 samples_offset += samples_to_copy;
1046 samples_to_process -= samples_to_copy;
1047
1048 // If buffer is full, encode and write frame
1049 if (enc->audio_partial_len >= enc->audio_frame_size * 2) {
1050 // Set up audio frame with accumulated samples
1051 asciichat_error_t prepare_result = encoder_prepare_audio_frame(enc);
1052 if (prepare_result != ASCIICHAT_OK)
1053 return prepare_result;
1054
1055 // Send frame to encoder
1056 int ret = avcodec_send_frame(enc->audio_codec_ctx, enc->audio_frame);
1057 if (ret < 0) {
1058 return SET_ERRNO(ERROR_MEDIA_INIT, "Cannot encode recording audio: %d", ret);
1059 } else {
1060 // Receive and write packets
1061 while (ret >= 0) {
1062 ret = avcodec_receive_packet(enc->audio_codec_ctx, enc->pkt);
1063 if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
1064 break;
1065 if (ret < 0) {
1066 return SET_ERRNO(ERROR_MEDIA_INIT, "ffmpeg: avcodec_receive_packet for audio failed: %d", ret);
1067 }
1068
1069 // Rescale timestamp and write
1070 av_packet_rescale_ts(enc->pkt, enc->audio_codec_ctx->time_base, enc->audio_stream->time_base);
1071 enc->pkt->stream_index = enc->audio_stream->index;
1072 ret = av_interleaved_write_frame(enc->fmt_ctx, enc->pkt);
1073 av_packet_unref(enc->pkt);
1074 if (ret < 0) {
1075 return SET_ERRNO(ERROR_FILE_OPERATION, "ffmpeg: av_interleaved_write_frame for audio failed: %d", ret);
1076 }
1077 }
1078 }
1079
1080 // Update PTS for next frame
1081 enc->audio_pts += enc->audio_frame_size;
1082 enc->audio_partial_len = 0;
1083 }
1084 }
1085
1086 return ASCIICHAT_OK;
1087}
@ ERROR_MEDIA_INIT
Definition error_codes.h:65
@ ERROR_FILE_OPERATION
#define EAGAIN
AVFrame * audio_frame
float * audio_partial_buf
AVStream * audio_stream
AVCodecContext * audio_codec_ctx

References ASCIICHAT_OK, ffmpeg_encoder_s::audio_codec_ctx, ffmpeg_encoder_s::audio_frame, ffmpeg_encoder_s::audio_frame_size, ffmpeg_encoder_s::audio_partial_buf, ffmpeg_encoder_s::audio_partial_len, ffmpeg_encoder_s::audio_pts, ffmpeg_encoder_s::audio_stream, EAGAIN, ERROR_FILE_OPERATION, ERROR_MEDIA_INIT, ffmpeg_encoder_s::fmt_ctx, ffmpeg_encoder_s::has_audio_stream, ffmpeg_encoder_s::pkt, and SET_ERRNO.

Referenced by render_file_destroy().

◆ ffmpeg_encoder_write_frame()

asciichat_error_t ffmpeg_encoder_write_frame ( ffmpeg_encoder_t * enc,
const uint8_t * rgb,
int pitch,
uint64_t captured_ns )

Write a single video frame to the output file.

The frame must be in RGBA format (8-bit per channel, 32-bit per pixel). Internal color space conversion is performed automatically based on the output format (see Encoder Options & Quality Tradeoffs for details).

Pitch Parameter: The pitch parameter specifies the byte offset from one row to the next. For tightly-packed RGBA data: pitch = width_px × 4. For padded/aligned buffers: pitch must reflect the actual row size.

Timestamp Parameter: The captured_ns parameter is the wall-clock timestamp (in nanoseconds) when the frame was captured. This ensures video playback uses actual capture timing, not encoder processing time. For correct frame timing in the output video, captured_ns should be the time when the frame was acquired from the media source.

Parameters
encEncoder handle (created with ffmpeg_encoder_create())
rgbPointer to frame data in RGBA format (not copied, used immediately)
pitchByte offset between rows (must match actual buffer layout)
captured_nsWall-clock timestamp in nanoseconds when frame was captured
Returns
ASCIICHAT_OK on success, error code otherwise
Note
Input frame data is NOT owned by the encoder. The caller must ensure the buffer remains valid throughout the call.
For image-only formats (PNG, JPEG), only the last frame written is kept. Previous frames are discarded when the next frame is written.
See also
ffmpeg_encoder_create, ffmpeg_encoder_destroy

Definition at line 825 of file ffmpeg_encoder.c.

826 {
827 if (!enc || !rgb)
828 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL input");
829
830 // Initialize first frame capture time on first call
831 if (enc->first_frame_captured_ns == 0) {
832 enc->first_frame_captured_ns = captured_ns;
833 log_debug("first frame captured at %llu ns", (unsigned long long)captured_ns);
834 }
835
836 log_debug_every(5 * US_PER_SEC_INT, "Encoding frame %d, %dx%d", enc->frame_count, enc->width_px, enc->height_px);
837
838 // Set up RGBA frame for conversion
839 uint8_t *data[1] = {(uint8_t *)rgb};
840 int linesize[1] = {pitch};
841
842 // Encoders may retain references to earlier frames for motion or transparency detection.
843 if (av_frame_make_writable(enc->frame_encoded) < 0)
844 return SET_ERRNO(ERROR_MEMORY, "ffmpeg: cannot make video frame writable");
845
846 // Ensure frame format is set before each sws_scale call.
847 // Some FFmpeg versions reset the format field, so we set it explicitly.
848 enc->frame_encoded->format = enc->target_pix_fmt;
849
850 // Convert RGBA to target format
851 uint64_t conversion_started_ns = time_get_ns();
852 sws_scale(enc->sws_ctx, (const uint8_t *const *)data, linesize, 0, enc->source_height, enc->frame_encoded->data,
853 enc->frame_encoded->linesize);
854 uint64_t converted_ns = time_get_ns();
855
856 // Set frame duration based on mode
857 int64_t frame_duration;
858 bool snapshot_mode = GET_OPTION(snapshot_mode);
859
860 if (enc->frame_count == 0) {
861 log_info("frame 0, snapshot_mode=%d", snapshot_mode);
862 }
863
864 // Use FPS-based frame duration (snapshot mode adjusts total duration in destroy())
865 frame_duration = enc->codec_ctx->time_base.den / (enc->codec_ctx->time_base.num * enc->fps);
866
867 enc->frame_encoded->duration = frame_duration;
868
869 // Track this frame's capture timestamp for duration calculation of next frame
870 if (snapshot_mode) {
871 uint64_t frame_dur_ms = (enc->previous_captured_ns > 0) ? (captured_ns - enc->previous_captured_ns) / 1000000 : 0;
872 if (enc->frame_count <= 2) {
873 log_info("ffmpeg: frame %d SET duration=%lld units (%.3fms since last, codec_base=1/%d)", enc->frame_count,
874 (long long)frame_duration, (double)frame_dur_ms, enc->codec_ctx->time_base.den);
875 }
876 enc->previous_captured_ns = captured_ns;
877 }
878
879 // Calculate PTS from actual capture timestamp
880 // time_base is {1, fps}, so 1 PTS unit = 1/fps seconds
881 // Convert elapsed nanoseconds to PTS units: elapsed_ns * fps / 1_000_000_000
882 uint64_t elapsed_ns = captured_ns - enc->first_frame_captured_ns;
883 int64_t pts_from_timestamp = (int64_t)((elapsed_ns * (uint64_t)enc->fps) / 1000000000ULL);
884
885 // Normal file rendering is constant-frame-rate. Wall-clock capture timestamps
886 // include scheduler jitter and can create gaps or duplicate PTS values, so use
887 // the encoded frame index for exact N / FPS duration. File snapshots with
888 // audio use this same timeline, excluding capture initialization delays.
889 // Live sources preserve wall-clock gaps between received frames.
890 if ((!snapshot_mode || enc->has_audio_stream) && !enc->live_timing) {
891 pts_from_timestamp = enc->frame_count;
892 }
893
894 // For snapshot sources without an estimated frame count, distribute frames
895 // linearly across the actual duration once the capture duration is known.
896 if (snapshot_mode && !enc->has_audio_stream && !enc->live_timing && enc->estimated_frame_count == 0) {
900 // Distribute frames linearly based on their capture timestamp relative to total duration
901 // pts = (frame_elapsed_ns / total_elapsed_ns) * actual_duration_sec * fps
902 // This ensures frame 0 is at 0s and last frame is at actual_duration_sec
903 double actual_duration_sec = (double)g_snapshot_actual_duration_ms / 1000.0;
904 double total_elapsed_sec = (double)g_snapshot_last_capture_elapsed_ns / (double)NS_PER_SEC_INT;
905
906 if (total_elapsed_sec > 0.001) {
907 double frame_fraction = (double)elapsed_ns / (double)g_snapshot_last_capture_elapsed_ns;
908 double target_pts_sec = frame_fraction * actual_duration_sec;
909 int64_t pts_before = pts_from_timestamp;
910 // Convert target seconds to time_base units: pts = target_sec * time_base.den / time_base.num
911 pts_from_timestamp =
912 (int64_t)(target_pts_sec * (double)enc->codec_ctx->time_base.den / (double)enc->codec_ctx->time_base.num);
913
914 if (enc->frame_count < 3 || enc->frame_count % 10 == 0) {
915 log_debug("ffmpeg: snapshot frame %d: PTS distributed (before=%lld, after=%lld, sec=%lld->%lld, frac=%.3f)",
916 enc->frame_count, (long long)pts_before, (long long)pts_from_timestamp,
917 (long long)((double)pts_before * enc->codec_ctx->time_base.num / enc->codec_ctx->time_base.den),
918 (long long)target_pts_sec, frame_fraction);
919 }
920 }
921 }
922 }
923
924 // Use timestamp-based (or scaled) PTS
925 enc->frame_encoded->pts = pts_from_timestamp;
926 enc->last_video_pts = pts_from_timestamp;
927 enc->video_pts += frame_duration;
928
929 if (!snapshot_mode) {
930 log_debug_every(US_PER_SEC_INT, "PTS calculation - elapsed_ns=%llu, pts_from_timestamp=%lld, fps=%d",
931 (unsigned long long)elapsed_ns, (long long)pts_from_timestamp, enc->fps);
932 }
933
934 // Send frame to encoder
935 int ret = avcodec_send_frame(enc->codec_ctx, enc->frame_encoded);
936 uint64_t submitted_ns = time_get_ns();
937 if (ret < 0) {
938 return SET_ERRNO(ERROR_MEDIA_INIT, "Cannot encode recording frame: %d", ret);
939 }
940
941 // Receive packets from encoder and write to file
942 static int write_frame_pkt_count = 0;
943
944 while (ret >= 0) {
945 ret = avcodec_receive_packet(enc->codec_ctx, enc->pkt);
946 if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
947 break;
948 if (ret < 0) {
949 return SET_ERRNO(ERROR_MEDIA_INIT, "ffmpeg: avcodec_receive_packet failed: %d", ret);
950 }
951
952 write_frame_pkt_count++;
953
954 // Set packet duration in codec time base (duration of one frame at target FPS)
955 // This ensures proper stream duration calculation
956 if (enc->pkt->duration == 0) {
957 enc->pkt->duration = enc->codec_ctx->time_base.den / (enc->codec_ctx->time_base.num * enc->fps);
958 }
959
960 av_packet_rescale_ts(enc->pkt, enc->codec_ctx->time_base, enc->stream->time_base);
961 enc->pkt->stream_index = enc->stream->index;
962
963 // In snapshot mode, apply adjusted duration if snapshot_actual_duration is set
964 // This happens when we get the actual duration from the capture thread
965 bool is_snapshot_mode = GET_OPTION(snapshot_mode);
966 if (is_snapshot_mode && !enc->has_audio_stream && enc->snapshot_actual_duration > 0 && enc->frame_count > 0) {
967 int64_t adjusted_dur = (int64_t)((enc->snapshot_actual_duration * enc->stream->time_base.den) / enc->frame_count);
968 // Only apply if it's significantly different from the default
969 if (adjusted_dur > enc->pkt->duration * 2) {
970 enc->pkt->duration = adjusted_dur;
971 log_debug("ffmpeg: write_frame packet %d duration adjusted to %lld units based on actual duration",
972 write_frame_pkt_count, (long long)enc->pkt->duration);
973 }
974 } else if (!is_snapshot_mode && write_frame_pkt_count < 3) {
975 log_debug("ffmpeg: write_frame pkt %d, duration=%lld, snapshot_mode=%d, actual_dur=%.2f, frame_count=%d",
976 write_frame_pkt_count, (long long)enc->pkt->duration, is_snapshot_mode, enc->snapshot_actual_duration,
977 enc->frame_count);
978 }
979
980 if (write_frame_pkt_count <= 2 || write_frame_pkt_count >= 16) {
983 "ffmpeg: write_frame pkt %d with final duration=%lld (%.6f sec in stream time_base), total frames=%d",
984 write_frame_pkt_count, (long long)enc->pkt->duration, (double)enc->pkt->duration / enc->stream->time_base.den,
985 enc->frame_count);
986 }
987
988 ret = av_interleaved_write_frame(enc->fmt_ctx, enc->pkt);
989 av_packet_unref(enc->pkt);
990 if (ret < 0) {
991 return SET_ERRNO(ERROR_FILE_OPERATION, "ffmpeg: av_interleaved_write_frame failed: %d", ret);
992 }
993
994 // Flush output buffer for stdout to ensure frames are written immediately
995 if (enc->is_stdout_pipe) {
996 avio_flush(enc->fmt_ctx->pb);
997 }
998 }
999
1000 uint64_t muxed_ns = time_get_ns();
1001 log_debug_every(US_PER_SEC_INT, "ENCODER_STAGE: convert_ms=%.2f submit_ms=%.2f packets_ms=%.2f",
1002 (double)(converted_ns - conversion_started_ns) / NS_PER_MS_INT,
1003 (double)(submitted_ns - converted_ns) / NS_PER_MS_INT,
1004 (double)(muxed_ns - submitted_ns) / NS_PER_MS_INT);
1005 enc->frame_count++;
1006 return ASCIICHAT_OK;
1007}
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
@ ERROR_MEMORY
Definition error_codes.h:51
#define NS_PER_SEC_INT
Definition time.h:169
#define NS_PER_MS_INT
Definition time.h:168
#define US_PER_SEC_INT
Definition time.h:173
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:749
uint64_t g_snapshot_last_capture_elapsed_ns
uint64_t g_snapshot_actual_duration_ms
struct SwsContext * sws_ctx
AVFrame * frame_encoded
AVCodecContext * codec_ctx
uint64_t previous_captured_ns

References ASCIICHAT_OK, ffmpeg_encoder_s::codec_ctx, EAGAIN, ERROR_FILE_OPERATION, ERROR_INVALID_PARAM, ERROR_MEDIA_INIT, ERROR_MEMORY, ffmpeg_encoder_s::estimated_frame_count, ffmpeg_encoder_s::first_frame_captured_ns, ffmpeg_encoder_s::fmt_ctx, ffmpeg_encoder_s::fps, ffmpeg_encoder_s::frame_count, ffmpeg_encoder_s::frame_encoded, g_snapshot_actual_duration_ms, g_snapshot_last_capture_elapsed_ns, GET_OPTION, ffmpeg_encoder_s::has_audio_stream, ffmpeg_encoder_s::height_px, ffmpeg_encoder_s::is_stdout_pipe, ffmpeg_encoder_s::last_video_pts, ffmpeg_encoder_s::live_timing, log_debug, log_debug_every, log_info, NS_PER_MS_INT, NS_PER_SEC_INT, ffmpeg_encoder_s::pkt, ffmpeg_encoder_s::previous_captured_ns, SET_ERRNO, ffmpeg_encoder_s::snapshot_actual_duration, ffmpeg_encoder_s::source_height, ffmpeg_encoder_s::stream, ffmpeg_encoder_s::sws_ctx, ffmpeg_encoder_s::target_pix_fmt, US_PER_SEC_INT, ffmpeg_encoder_s::video_pts, and ffmpeg_encoder_s::width_px.