#include "media.h" #include "buffers.h" #include "common.h" #include "ecex.h" #include "path.h" #include "util.h" #include #include #include #include #include extern FILE *popen(const char *command, const char *type); extern int pclose(FILE *stream); extern int mkstemp(char *template); static char *shell_quote(const char *path) { if (!path) return NULL; size_t len = 2; /* quotes */ for (const char *p = path; *p; p++) len += (*p == '\'') ? 4 : 1; char *out = malloc(len + 1); if (!out) return NULL; char *w = out; *w++ = '\''; for (const char *p = path; *p; p++) { if (*p == '\'') { memcpy(w, "'\\''", 4); w += 4; } else { *w++ = *p; } } *w++ = '\''; *w = '\0'; return out; } static int ppm_skip_ws_and_comments(FILE *f) { int c = 0; do { c = fgetc(f); if (c == '#') { while (c != '\n' && c != EOF) c = fgetc(f); } } while (c != EOF && isspace((unsigned char)c)); if (c == EOF) return EOF; ungetc(c, f); return 0; } static int ppm_read_int(FILE *f, int *out) { if (!f || !out) return ECEX_ERR; if (ppm_skip_ws_and_comments(f) == EOF) return ECEX_ERR; int c = fgetc(f); if (c == EOF || !isdigit((unsigned char)c)) return ECEX_ERR; int value = 0; while (c != EOF && isdigit((unsigned char)c)) { value = value * 10 + (c - '0'); c = fgetc(f); } if (c != EOF) ungetc(c, f); *out = value; return ECEX_OK; } static int read_ppm_frame(FILE *f, int *out_w, int *out_h, unsigned char **out_rgba) { if (!f || !out_w || !out_h || !out_rgba) return ECEX_ERR; int p = fgetc(f); int six = fgetc(f); if (p == EOF || six == EOF) return ECEX_ERR; if (p != 'P' || six != '6') return ECEX_ERR; int w = 0, h = 0, maxv = 0; if (ppm_read_int(f, &w) != ECEX_OK || ppm_read_int(f, &h) != ECEX_OK || ppm_read_int(f, &maxv) != ECEX_OK) { return ECEX_ERR; } if (w <= 0 || h <= 0 || maxv <= 0 || maxv > 255) return ECEX_ERR; int c = fgetc(f); if (c == EOF) return ECEX_ERR; if (!isspace((unsigned char)c)) ungetc(c, f); size_t rgb_len = (size_t)w * (size_t)h * 3u; size_t rgba_len = (size_t)w * (size_t)h * 4u; if (w > 8192 || h > 8192 || rgb_len / 3u != (size_t)w * (size_t)h) return ECEX_ERR; unsigned char *rgb = malloc(rgb_len); unsigned char *rgba = malloc(rgba_len); if (!rgb || !rgba) { free(rgb); free(rgba); return ECEX_ERR; } size_t got = fread(rgb, 1, rgb_len, f); if (got != rgb_len) { free(rgb); free(rgba); return ECEX_ERR; } for (size_t i = 0, j = 0; i < rgb_len; i += 3, j += 4) { rgba[j + 0] = rgb[i + 0]; rgba[j + 1] = rgb[i + 1]; rgba[j + 2] = rgb[i + 2]; rgba[j + 3] = 255; } free(rgb); *out_w = w; *out_h = h; *out_rgba = rgba; return ECEX_OK; } static int set_buffer_pixels(buffer_t *buffer, int w, int h, unsigned char *rgba) { if (!buffer || w <= 0 || h <= 0 || !rgba) { free(rgba); return ECEX_ERR; } free(buffer->media_pixels); buffer->media_pixels = rgba; buffer->media_width = w; buffer->media_height = h; buffer->media_dirty = 1; return ECEX_OK; } static char *make_temp_log_path(void) { char tmpl[] = "/tmp/ecex-ffmpeg-XXXXXX"; int fd = mkstemp(tmpl); if (fd < 0) return NULL; close(fd); return ecex_strdup(tmpl); } static char *read_decode_log_status(const char *log_path, const char *fallback) { FILE *f = log_path ? fopen(log_path, "rb") : NULL; if (!f) return ecex_strdup(fallback ? fallback : "ffmpeg did not decode a preview frame."); char body[1400]; size_t n = fread(body, 1, sizeof(body) - 1, f); fclose(f); body[n] = '\0'; if (n == 0) return ecex_strdup(fallback ? fallback : "ffmpeg did not decode a preview frame."); char *out = malloc(n + 128); if (!out) return ecex_strdup(fallback ? fallback : "ffmpeg did not decode a preview frame."); snprintf(out, n + 128, "ffmpeg did not decode a preview frame.\n\nffmpeg stderr:\n%s", body); return out; } static char *make_decode_command(const char *path, int video, char **out_log_path) { char *q = shell_quote(path); if (!q) return NULL; char *log_path = make_temp_log_path(); char *qlog = log_path ? shell_quote(log_path) : NULL; const char *ffmpeg = getenv("ECEX_FFMPEG"); if (!ffmpeg || !*ffmpeg) ffmpeg = "ffmpeg"; /* * Important: ffmpeg's filtergraph parser treats ',' as a filter * separator unless it is escaped, even when the expression is inside * quotes. The previous command used min(%d,iw), which makes many * ffmpeg builds fail before producing a single PPM frame. Keep the * comma escaped in the command string that reaches ffmpeg. * * -nostdin also prevents ffmpeg from accidentally consuming editor * terminal input when ecex is launched from a shell. */ const char *fmt = video ? "%s -nostdin -hide_banner -loglevel error -i %s -an -vf \"fps=60,scale='min(%d\\,iw)':-2\" -f image2pipe -vcodec ppm - %s%s" : "%s -nostdin -hide_banner -loglevel error -i %s -frames:v 1 -vf \"scale='min(%d\\,iw)':-2\" -f image2pipe -vcodec ppm - %s%s"; int need = snprintf(NULL, 0, fmt, ffmpeg, q, ECEX_MEDIA_MAX_DIMENSION, qlog ? "2>" : "", qlog ? qlog : ""); char *cmd = malloc((size_t)need + 1); if (cmd) { snprintf(cmd, (size_t)need + 1, fmt, ffmpeg, q, ECEX_MEDIA_MAX_DIMENSION, qlog ? "2>" : "", qlog ? qlog : ""); } if (out_log_path) { *out_log_path = log_path; log_path = NULL; } free(qlog); free(log_path); free(q); return cmd; } static void buffer_set_media_text(buffer_t *buffer, const char *path, int video, const char *status) { if (!buffer) return; buffer->read_only = 0; buffer_clear(buffer); char text[2048]; snprintf(text, sizeof(text), "%s preview: %s\n\n%s\n\nRequirements:\n install ffmpeg/ffprobe in PATH for broad image and video decoding.\n\nControls:\n Space / p play-pause video\n q quit window\n", video ? "Video" : "Image", path ? path : "(unknown)", status ? status : "No decoded frame available."); buffer_append(buffer, text); buffer->modified = 0; buffer->read_only = 1; } void ecex_media_buffer_clear(buffer_t *buffer) { if (!buffer) return; if (buffer->media_pipe) { pclose((FILE *)buffer->media_pipe); buffer->media_pipe = NULL; } free(buffer->media_path); buffer->media_path = NULL; free(buffer->media_pixels); buffer->media_pixels = NULL; buffer->media_kind = ECEX_MEDIA_NONE; buffer->media_width = 0; buffer->media_height = 0; buffer->media_dirty = 0; buffer->media_texture_width = 0; buffer->media_texture_height = 0; buffer->media_last_frame_time = 0.0; buffer->media_playing = 0; buffer->media_status[0] = '\0'; } int ecex_media_buffer_has_pixels(buffer_t *buffer) { return buffer && buffer->media_pixels && buffer->media_width > 0 && buffer->media_height > 0; } static buffer_t *media_buffer_for_path(ecex_t *ed, const char *path, int video) { if (!ed || !path) return NULL; char *base = ecex_path_basename_dup(path); if (!base) return NULL; char name[1024]; snprintf(name, sizeof(name), "*%s-preview:%s*", video ? "video" : "image", base); free(base); buffer_t *buffer = ecex_find_buffer(ed, name); if (!buffer) buffer = ecex_create_buffer(ed, name, path, 1); if (!buffer) return NULL; return buffer; } int ecex_media_load_into_buffer(ecex_t *ed, const char *path, buffer_t *buffer) { if (!ed || !path || !buffer || !ecex_path_is_media(path)) return ECEX_ERR; int video = ecex_path_is_video(path); ecex_media_buffer_clear(buffer); buffer->media_kind = video ? ECEX_MEDIA_VIDEO : ECEX_MEDIA_IMAGE; ecex_buffer_set_major_mode_by_name(ed, buffer, "media-preview-mode"); buffer->media_path = ecex_path_normalize(path); if (!buffer->media_path) buffer->media_path = ecex_strdup(path); buffer->read_only = 0; buffer_clear(buffer); buffer_append(buffer, video ? "Loading video preview...\n" : "Loading image preview...\n"); buffer->modified = 0; buffer->read_only = 1; char *log_path = NULL; char *cmd = make_decode_command(buffer->media_path ? buffer->media_path : path, video, &log_path); if (!cmd) { buffer_set_media_text(buffer, path, video, "Could not allocate ffmpeg command."); free(log_path); return ECEX_ERR; } FILE *pipe = popen(cmd, "r"); free(cmd); if (!pipe) { buffer_set_media_text(buffer, path, video, "Could not launch ffmpeg."); if (log_path) unlink(log_path); free(log_path); return ECEX_ERR; } int w = 0, h = 0; unsigned char *rgba = NULL; if (read_ppm_frame(pipe, &w, &h, &rgba) != ECEX_OK) { pclose(pipe); char *status = read_decode_log_status(log_path, "ffmpeg did not decode a preview frame."); buffer_set_media_text(buffer, path, video, status); free(status); if (log_path) unlink(log_path); free(log_path); return ECEX_ERR; } if (log_path) unlink(log_path); free(log_path); if (video) { buffer->media_pipe = pipe; buffer->media_playing = 1; snprintf(buffer->media_status, sizeof(buffer->media_status), "Playing at decoded 60fps stream"); } else { pclose(pipe); snprintf(buffer->media_status, sizeof(buffer->media_status), "Image decoded via ffmpeg"); } set_buffer_pixels(buffer, w, h, rgba); buffer->read_only = 0; buffer_clear(buffer); char info[1024]; snprintf(info, sizeof(info), "%s preview: %s\n%d x %d\n%s\n\n%s\n", video ? "Video" : "Image", buffer->media_path ? buffer->media_path : path, w, h, buffer->media_status, video ? "Space/p toggles playback. q quits window." : "q quits window."); buffer_append(buffer, info); buffer->modified = 0; buffer->read_only = 1; return ECEX_OK; } int ecex_media_open(ecex_t *ed, const char *path) { if (!ed || !path || !ecex_path_is_media(path)) return ECEX_ERR; int video = ecex_path_is_video(path); buffer_t *buffer = media_buffer_for_path(ed, path, video); if (!buffer) return ECEX_ERR; ecex_media_load_into_buffer(ed, path, buffer); return ecex_switch_buffer(ed, buffer->name); } int ecex_media_toggle_playback(ecex_t *ed) { buffer_t *buffer = ecex_current_buffer(ed); if (!buffer || buffer->media_kind != ECEX_MEDIA_VIDEO) return ECEX_ERR; buffer->media_playing = !buffer->media_playing; snprintf(buffer->media_status, sizeof(buffer->media_status), "%s", buffer->media_playing ? "Playing" : "Paused"); return ECEX_OK; } int ecex_media_tick(ecex_t *ed, double now_seconds) { if (!ed) return 0; int dirty = 0; for (size_t i = 0; i < ed->buffer_count; i++) { buffer_t *buffer = ed->buffers[i]; if (!buffer || buffer->media_kind != ECEX_MEDIA_VIDEO || !buffer->media_pipe || !buffer->media_playing) continue; if (buffer->media_last_frame_time > 0.0 && now_seconds - buffer->media_last_frame_time < (1.0 / 60.0)) continue; int w = 0, h = 0; unsigned char *rgba = NULL; if (read_ppm_frame((FILE *)buffer->media_pipe, &w, &h, &rgba) == ECEX_OK) { set_buffer_pixels(buffer, w, h, rgba); buffer->media_last_frame_time = now_seconds; dirty = 1; } else { pclose((FILE *)buffer->media_pipe); buffer->media_pipe = NULL; buffer->media_playing = 0; snprintf(buffer->media_status, sizeof(buffer->media_status), "Playback finished"); dirty = 1; } } return dirty; }