diff options
Diffstat (limited to 'src/media.c')
| -rw-r--r-- | src/media.c | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/src/media.c b/src/media.c new file mode 100644 index 0000000..6c7a989 --- /dev/null +++ b/src/media.c @@ -0,0 +1,378 @@ +#include "media.h" + +#include "buffers.h" +#include "common.h" +#include "ecex.h" +#include "path.h" +#include "util.h" + +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +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; +} |
