ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
encoder.c
Go to the documentation of this file.
1
7#include <ascii-chat/common.h>
9#include <ascii-chat/log/io.h>
12
13#include <libavcodec/avcodec.h>
14#include <libavutil/frame.h>
15#include <libavutil/imgutils.h>
16#include <libavutil/opt.h>
17
18#include <string.h>
19#include <stdlib.h>
20#include <unistd.h>
21
22static int h265_thread_count(void) {
23#ifdef _WIN32
24 SYSTEM_INFO sysinfo;
25 GetSystemInfo(&sysinfo);
26 long cores = (long)sysinfo.dwNumberOfProcessors;
27#else
28 long cores = sysconf(_SC_NPROCESSORS_ONLN);
29#endif
30 if (cores <= 0)
31 cores = 4;
32 int half = (int)(cores / 2);
33 return half > 4 ? half : 4;
34}
35
54
55h265_encoder_t *h265_encoder_create(uint16_t initial_width, uint16_t initial_height) {
56 if (initial_width == 0 || initial_height == 0) {
57 SET_ERRNO(ERROR_MEDIA_INIT, "Frame dimensions must be non-zero");
58 return NULL;
59 }
60
62 if (!enc) {
63 SET_ERRNO(ERROR_MEMORY, "Failed to allocate encoder structure");
64 return NULL;
65 }
66
67 // Find best available HEVC encoder (try by ID first, then by name for specific implementations)
68 const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_HEVC);
69
70 // If generic HEVC encoder not found, try specific implementations by name
71 if (!codec) {
72 codec = avcodec_find_encoder_by_name("libx265");
73 }
74 if (!codec) {
75 codec = avcodec_find_encoder_by_name("hevc_vaapi");
76 }
77 if (!codec) {
78 codec = avcodec_find_encoder_by_name("hevc_videotoolbox");
79 }
80 if (!codec) {
81 codec = avcodec_find_encoder_by_name("hevc_nvenc");
82 }
83 if (!codec) {
84 codec = avcodec_find_encoder_by_name("hevc_qsv");
85 }
86
87 if (!codec) {
88 SET_ERRNO(ERROR_MEDIA_INIT, "HEVC encoder not found");
89 SAFE_FREE(enc);
90 return NULL;
91 }
92
93 enc->codec_ctx = avcodec_alloc_context3(codec);
94 if (!enc->codec_ctx) {
95 SET_ERRNO(ERROR_MEMORY, "Failed to allocate codec context");
96 SAFE_FREE(enc);
97 return NULL;
98 }
99
100 // Configure codec context
101 enc->codec_ctx->width = initial_width;
102 enc->codec_ctx->height = initial_height;
103 enc->codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
104 enc->codec_ctx->time_base = (AVRational){1, 30};
105 enc->codec_ctx->framerate = (AVRational){30, 1};
106
107 // Set ultrafast preset for low-latency encoding
108 if (av_opt_set(enc->codec_ctx->priv_data, "preset", "ultrafast", 0) < 0) {
109 log_warn("Failed to set preset to ultrafast, using default");
110 }
111 if (av_opt_set(enc->codec_ctx->priv_data, "tune", "zerolatency", 0) < 0) {
112 log_warn("Failed to set tune to zerolatency, using default");
113 }
114
115 enc->codec_ctx->thread_count = h265_thread_count();
116
117 // Defer avcodec_open2 to first encode call. The initial resolution (800x600) is
118 // usually wrong — the actual frame resolution is only known when the first capture
119 // arrives. Opening the codec here would create an x265 thread pool (~1s) that gets
120 // immediately discarded on reconfigure, wasting startup time.
121 enc->codec_opened = false;
122
123 // Allocate frame and packet
124 enc->frame = av_frame_alloc();
125 enc->packet = av_packet_alloc();
126
127 if (!enc->frame || !enc->packet) {
128 SET_ERRNO(ERROR_MEMORY, "Failed to allocate frame/packet");
129 if (enc->frame)
130 av_frame_free(&enc->frame);
131 if (enc->packet)
132 av_packet_free(&enc->packet);
133 avcodec_free_context(&enc->codec_ctx);
134 SAFE_FREE(enc);
135 return NULL;
136 }
137
138 enc->current_width = initial_width;
139 enc->current_height = initial_height;
140
141 // Allocate YUV420P buffer (Y plane + U/V planes)
142 size_t stride = initial_width;
143 enc->yuv_buf_size = stride * initial_height * 3 / 2;
144 enc->yuv_buf = SAFE_MALLOC(enc->yuv_buf_size, uint8_t *);
145 if (!enc->yuv_buf) {
146 SET_ERRNO(ERROR_MEMORY, "Failed to allocate YUV buffer");
147 av_frame_free(&enc->frame);
148 av_packet_free(&enc->packet);
149 avcodec_free_context(&enc->codec_ctx);
150 SAFE_FREE(enc);
151 return NULL;
152 }
153
154 log_info("FFmpeg HEVC encoder created: %ux%u (codec: %s)", initial_width, initial_height, codec->name);
155
156 // Register encoder for debugging
157 NAMED_REGISTER_H265_ENCODER(enc, "h265_encoder", NULL);
158
159 return enc;
160}
161
163 if (!encoder)
164 return;
165
166 if (encoder->frame) {
167 av_frame_free(&encoder->frame);
168 }
169 if (encoder->packet) {
170 av_packet_free(&encoder->packet);
171 }
172 if (encoder->codec_ctx) {
173 // Capture x265 encoder cleanup output (final frame stats, etc.)
174 LOG_IO("hevc", { avcodec_free_context(&encoder->codec_ctx); });
175 }
176 if (encoder->yuv_buf) {
177 SAFE_FREE(encoder->yuv_buf);
178 }
179 SAFE_FREE(encoder);
180}
181
182static asciichat_error_t h265_encoder_open_codec(h265_encoder_t *encoder, uint16_t width, uint16_t height) {
183 // Free existing context if re-opening at a different resolution
184 if (encoder->codec_ctx) {
185 avcodec_free_context(&encoder->codec_ctx);
186 }
187
188 const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_HEVC);
189 encoder->codec_ctx = avcodec_alloc_context3(codec);
190 if (!encoder->codec_ctx) {
191 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate codec context for %ux%u", width, height);
192 }
193
194 encoder->codec_ctx->width = width;
195 encoder->codec_ctx->height = height;
196 encoder->codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
197 encoder->codec_ctx->time_base = (AVRational){1, 30};
198 encoder->codec_ctx->framerate = (AVRational){30, 1};
199 encoder->codec_ctx->thread_count = h265_thread_count();
200
201 if (av_opt_set(encoder->codec_ctx->priv_data, "preset", "ultrafast", 0) < 0) {
202 log_warn("Failed to set preset to ultrafast");
203 }
204 if (av_opt_set(encoder->codec_ctx->priv_data, "tune", "zerolatency", 0) < 0) {
205 log_warn("Failed to set tune to zerolatency");
206 }
207
208 log_dev("[H265_OPEN] avcodec_open2 for %ux%u (threads=%d)", width, height, encoder->codec_ctx->thread_count);
209 int codec_open_result = 0;
210 LOG_IO("hevc", { codec_open_result = avcodec_open2(encoder->codec_ctx, codec, NULL); });
211
212 if (codec_open_result < 0) {
213 return SET_ERRNO(ERROR_MEDIA_INIT, "Failed to open HEVC encoder for %ux%u", width, height);
214 }
215
216 // Resize YUV buffer if needed
217 size_t new_yuv_size = width * height * 3 / 2;
218 if (new_yuv_size > encoder->yuv_buf_size) {
219 uint8_t *new_buf = SAFE_MALLOC(new_yuv_size, uint8_t *);
220 if (!new_buf) {
221 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate YUV buffer for %ux%u", width, height);
222 }
223 SAFE_FREE(encoder->yuv_buf);
224 encoder->yuv_buf = new_buf;
225 encoder->yuv_buf_size = new_yuv_size;
226 }
227
228 encoder->current_width = width;
229 encoder->current_height = height;
230 encoder->codec_opened = true;
231 encoder->request_keyframe = true;
232
233 log_info("HEVC encoder opened for %ux%u", width, height);
234 return ASCIICHAT_OK;
235}
236
237static asciichat_error_t h265_encoder_reconfigure(h265_encoder_t *encoder, uint16_t new_width, uint16_t new_height) {
238 if (encoder->codec_opened && encoder->current_width == new_width && encoder->current_height == new_height) {
239 return ASCIICHAT_OK;
240 }
241 return h265_encoder_open_codec(encoder, new_width, new_height);
242}
243
244static void h265_encoder_ascii_to_yuv420(const uint8_t *rgb_data, uint16_t width, uint16_t height, uint8_t *yuv_buf) {
245 uint8_t *y_plane = yuv_buf;
246 uint8_t *u_plane = y_plane + width * height;
247 uint8_t *v_plane = u_plane + (width / 2) * (height / 2);
248
249 // Convert RGB24 to YUV420p
250 // Y = 0.299*R + 0.587*G + 0.114*B (standard luma formula)
251 // U = -0.14713*R - 0.28886*G + 0.436*B (offset by 128)
252 // V = 0.615*R - 0.51499*G - 0.10001*B (offset by 128)
253
254 // Process Y plane (each pixel)
255 for (uint32_t i = 0; i < (uint32_t)width * (uint32_t)height; i++) {
256 uint8_t r = rgb_data[i * 3];
257 uint8_t g = rgb_data[i * 3 + 1];
258 uint8_t b = rgb_data[i * 3 + 2];
259
260 // Calculate Y (luma)
261 y_plane[i] = (uint8_t)(((66 * r + 129 * g + 25 * b + 128) >> 8) + 16);
262 }
263
264 // Process U and V planes (subsampled 2x2)
265 for (uint32_t y = 0; y < (uint32_t)height; y += 2) {
266 for (uint32_t x = 0; x < (uint32_t)width; x += 2) {
267 // Average 2x2 RGB block for chroma
268 uint32_t idx00 = ((y)*width + x) * 3;
269 uint32_t idx10 = ((y)*width + x + 1) * 3;
270 uint32_t idx01 = ((y + 1) * width + x) * 3;
271 uint32_t idx11 = ((y + 1) * width + x + 1) * 3;
272
273 uint16_t r_avg = (rgb_data[idx00] + rgb_data[idx10] + rgb_data[idx01] + rgb_data[idx11]) / 4;
274 uint16_t g_avg = (rgb_data[idx00 + 1] + rgb_data[idx10 + 1] + rgb_data[idx01 + 1] + rgb_data[idx11 + 1]) / 4;
275 uint16_t b_avg = (rgb_data[idx00 + 2] + rgb_data[idx10 + 2] + rgb_data[idx01 + 2] + rgb_data[idx11 + 2]) / 4;
276
277 // Calculate U (Cb)
278 int16_t u = (int16_t)((((-38 * r_avg - 74 * g_avg + 112 * b_avg + 128) >> 8) + 128));
279 u_plane[(y / 2) * (width / 2) + (x / 2)] = (uint8_t)(u < 0 ? 0 : (u > 255 ? 255 : u));
280
281 // Calculate V (Cr)
282 int16_t v = (int16_t)((((112 * r_avg - 94 * g_avg - 18 * b_avg + 128) >> 8) + 128));
283 v_plane[(y / 2) * (width / 2) + (x / 2)] = (uint8_t)(v < 0 ? 0 : (v > 255 ? 255 : v));
284 }
285 }
286}
287
288asciichat_error_t h265_encode(h265_encoder_t *encoder, uint16_t width, uint16_t height, const uint8_t *ascii_data,
289 uint8_t *output_buf, size_t *output_size) {
290 log_info("[H265_ENCODE_1] Starting h265_encode %ux%u", width, height);
291 if (!encoder || !ascii_data || !output_buf || !output_size) {
292 return SET_ERRNO(ERROR_INTERNAL, "Invalid encoder arguments");
293 }
294
295 if (*output_size < 5) {
296 return SET_ERRNO(ERROR_NETWORK_SIZE, "Output buffer too small (minimum 5 bytes)");
297 }
298
299 log_info("[H265_ENCODE_2] Reconfigure %ux%u -> %ux%u", encoder->current_width, encoder->current_height, width,
300 height);
301 asciichat_error_t result = h265_encoder_reconfigure(encoder, width, height);
302 log_info("[H265_ENCODE_3] Reconfigure returned %d", result);
303 if (result != ASCIICHAT_OK) {
304 return result;
305 }
306
307 log_info("[H265_ENCODE_4] Converting ASCII to YUV420");
308 h265_encoder_ascii_to_yuv420(ascii_data, width, height, encoder->yuv_buf);
309 log_info("[H265_ENCODE_5] YUV420 conversion done");
310
311 // Setup frame data pointers
312 encoder->frame->data[0] = encoder->yuv_buf;
313 encoder->frame->data[1] = encoder->yuv_buf + width * height;
314 encoder->frame->data[2] = encoder->yuv_buf + width * height + (width / 2) * (height / 2);
315 encoder->frame->linesize[0] = width;
316 encoder->frame->linesize[1] = width / 2;
317 encoder->frame->linesize[2] = width / 2;
318 encoder->frame->width = width;
319 encoder->frame->height = height;
320 encoder->frame->format = AV_PIX_FMT_YUV420P;
321
322 // Request keyframe if needed
323 if (encoder->request_keyframe) {
324 encoder->frame->pict_type = AV_PICTURE_TYPE_I;
325 encoder->request_keyframe = false;
326 }
327
328 // Encode frame
329 log_info("[H265_ENCODE_6] Sending frame to encoder: %ux%u format=%d", width, height, encoder->frame->format);
330 int send_ret = avcodec_send_frame(encoder->codec_ctx, encoder->frame);
331 log_info("[H265_ENCODE_7] avcodec_send_frame returned %d", send_ret);
332 if (send_ret < 0) {
333 log_error("H265_ENCODE: avcodec_send_frame failed: %d", send_ret);
334 return SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to send frame to encoder");
335 }
336
337 log_info("[H265_ENCODE_8] Receiving encoded packet...");
338 int recv_ret = avcodec_receive_packet(encoder->codec_ctx, encoder->packet);
339 if (recv_ret == AVERROR(EAGAIN)) {
340 // EAGAIN is normal - encoder needs more frames before outputting a packet
341 log_dev("H265_ENCODE: EAGAIN - encoder buffering frames (normal), waiting for next frame");
342 *output_size = 0; // No output this time
343 return ASCIICHAT_OK;
344 }
345 if (recv_ret < 0) {
346 log_error("H265_ENCODE: avcodec_receive_packet failed: %d (EAGAIN=%d)", recv_ret, AVERROR(EAGAIN));
347 return SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to receive encoded packet");
348 }
349 log_debug("H265_ENCODE: Got packet: size=%d", (int)encoder->packet->size);
350
351 uint8_t flags = 0;
352 if (encoder->frame->pict_type == AV_PICTURE_TYPE_I) {
354 encoder->keyframes++;
355 }
356
357 if (width != encoder->current_width || height != encoder->current_height) {
359 }
360
361 size_t header_size = 5;
362 size_t packet_size = encoder->packet->size;
363 size_t required_size = header_size + packet_size;
364
365 if (required_size > *output_size) {
366 return SET_ERRNO(ERROR_NETWORK_SIZE, "Output buffer too small: need %zu, have %zu", required_size, *output_size);
367 }
368
369 // Write header: [flags][width:u16][height:u16]
370 output_buf[0] = flags;
371 output_buf[1] = (width >> 8) & 0xFF;
372 output_buf[2] = width & 0xFF;
373 output_buf[3] = (height >> 8) & 0xFF;
374 output_buf[4] = height & 0xFF;
375
376 // Write encoded data
377 if (packet_size > 0) {
378 memcpy(output_buf + header_size, encoder->packet->data, packet_size);
379 }
380
381 *output_size = required_size;
382 encoder->total_frames++;
383 log_dev("[H265_ENCODE_DONE] Encoding complete, output_size=%zu", required_size);
384
385 return ASCIICHAT_OK;
386}
387
389 if (encoder) {
390 encoder->request_keyframe = true;
391 }
392}
393
394asciichat_error_t h265_encoder_flush(h265_encoder_t *encoder, uint8_t *output_buf, size_t *output_size) {
395 if (!encoder || !output_buf || !output_size) {
396 return SET_ERRNO(ERROR_INTERNAL, "Invalid encoder or output parameters for flush");
397 }
398
399 if (*output_size < 5) {
400 return SET_ERRNO(ERROR_NETWORK_SIZE, "Output buffer too small (minimum 5 bytes)");
401 }
402
403 // Send NULL frame to signal end of stream (flushes encoder)
404 log_debug("H265_FLUSH: Sending NULL frame to encoder to flush buffered data");
405 int send_ret = avcodec_send_frame(encoder->codec_ctx, NULL);
406 if (send_ret < 0) {
407 log_error("H265_FLUSH: avcodec_send_frame(NULL) failed: %d", send_ret);
408 return SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to flush encoder");
409 }
410
411 // Try to receive a packet
412 log_debug("H265_FLUSH: Attempting to receive flushed packet...");
413 int recv_ret = avcodec_receive_packet(encoder->codec_ctx, encoder->packet);
414 if (recv_ret == AVERROR(EAGAIN)) {
415 log_dev("H265_FLUSH: EAGAIN - no more frames available");
416 *output_size = 0;
417 return ASCIICHAT_OK;
418 }
419 if (recv_ret == AVERROR_EOF) {
420 log_debug("H265_FLUSH: EOF - encoder is flushed");
421 *output_size = 0;
422 return ASCIICHAT_OK;
423 }
424 if (recv_ret < 0) {
425 log_error("H265_FLUSH: avcodec_receive_packet failed: %d", recv_ret);
426 return SET_ERRNO(ERROR_MEDIA_DECODE, "Failed to receive flushed packet");
427 }
428
429 log_debug("H265_FLUSH: Got flushed packet: size=%d", (int)encoder->packet->size);
430
431 // Check if packet size exceeds output buffer
432 size_t header_size = 5;
433 size_t packet_size = encoder->packet->size;
434 size_t required_size = header_size + packet_size;
435
436 if (required_size > *output_size) {
437 return SET_ERRNO(ERROR_NETWORK_SIZE, "Output buffer too small: need %zu, have %zu", required_size, *output_size);
438 }
439
440 // Write header: [flags][width:u16][height:u16]
441 uint8_t flags = 0;
442 if (encoder->frame->pict_type == AV_PICTURE_TYPE_I) {
444 encoder->keyframes++;
445 }
446
447 output_buf[0] = flags;
448 output_buf[1] = (encoder->current_width >> 8) & 0xFF;
449 output_buf[2] = encoder->current_width & 0xFF;
450 output_buf[3] = (encoder->current_height >> 8) & 0xFF;
451 output_buf[4] = encoder->current_height & 0xFF;
452
453 // Write encoded data
454 if (packet_size > 0) {
455 memcpy(output_buf + header_size, encoder->packet->data, packet_size);
456 }
457
458 *output_size = required_size;
459 encoder->total_frames++;
460
461 return ASCIICHAT_OK;
462}
463
464void h265_encoder_get_stats(h265_encoder_t *encoder, uint64_t *total_frames, uint64_t *keyframes,
465 uint32_t *avg_bitrate) {
466 if (!encoder)
467 return;
468
469 if (total_frames)
470 *total_frames = encoder->total_frames;
471 if (keyframes)
472 *keyframes = encoder->keyframes;
473 if (avg_bitrate)
474 *avg_bitrate = encoder->avg_bitrate;
475}
⚙️ Common definitions, error codes, macros, and types shared throughout the application
void h265_encoder_t
Named object registry for debugging — log identifiable resource names.
asciichat_error_t h265_encode(h265_encoder_t *encoder, uint16_t width, uint16_t height, const uint8_t *ascii_data, uint8_t *output_buf, size_t *output_size)
Definition encoder.c:288
void h265_encoder_get_stats(h265_encoder_t *encoder, uint64_t *total_frames, uint64_t *keyframes, uint32_t *avg_bitrate)
Definition encoder.c:464
h265_encoder_t * h265_encoder_create(uint16_t initial_width, uint16_t initial_height)
Definition encoder.c:55
asciichat_error_t h265_encoder_flush(h265_encoder_t *encoder, uint8_t *output_buf, size_t *output_size)
Definition encoder.c:394
void h265_encoder_destroy(h265_encoder_t *encoder)
Definition encoder.c:162
void h265_encoder_request_keyframe(h265_encoder_t *encoder)
Definition encoder.c:388
struct h265_encoder h265_encoder_t
HEVC/H.265 encoder for ASCII art frames using libx265.
#define H265_ENCODER_FLAG_KEYFRAME
Definition encoder.h:33
#define H265_ENCODER_FLAG_SIZE_CHANGE
Definition encoder.h:34
unsigned short uint16_t
Definition common.h:57
unsigned int uint32_t
Definition common.h:58
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define NAMED_REGISTER_H265_ENCODER(encoder, name, parent_ptr)
Register an H.265 encoder with automatic format specifier.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_MEDIA_DECODE
Definition error_codes.h:72
@ ERROR_NETWORK_SIZE
Definition error_codes.h:82
@ ERROR_MEDIA_INIT
Definition error_codes.h:70
@ ERROR_INTERNAL
Definition error_codes.h:92
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define EAGAIN
Capture stdout/stderr and redirect to logging system.
#define LOG_IO(prefix, block)
Capture output from a code block and log it.
Definition io.h:81
uint64_t keyframes
Definition encoder.c:51
uint64_t total_frames
Definition encoder.c:50
uint16_t current_height
Definition encoder.c:42
bool codec_opened
Definition encoder.c:47
AVFrame * frame
Definition encoder.c:38
uint16_t current_width
Definition encoder.c:41
uint8_t * yuv_buf
Definition encoder.c:44
AVCodecContext * codec_ctx
Definition encoder.c:37
bool request_keyframe
Definition encoder.c:48
AVPacket * packet
Definition encoder.c:39
size_t yuv_buf_size
Definition encoder.c:45
uint32_t avg_bitrate
Definition encoder.c:52
Cross-platform system functions interface for ascii-chat.
⏱️ High-precision timing utilities using sokol_time.h and uthash