summaryrefslogtreecommitdiff
path: root/src/video.c
diff options
context:
space:
mode:
authorDoan Luu Duc Tai <doan@luuduct.ai>2026-09-08 14:46:47 -0500
committerDoan Luu Duc Tai <doan@luuduct.ai>2026-09-08 14:46:47 -0500
commitf34fb5042d40c186583f9e3009d8a1dd71d07d43 (patch)
tree3f494f3723ebe9f173d0702a97509794f0ef85a5 /src/video.c
Initial commitmain
Diffstat (limited to 'src/video.c')
-rw-r--r--src/video.c261
1 files changed, 261 insertions, 0 deletions
diff --git a/src/video.c b/src/video.c
new file mode 100644
index 0000000..6bfcff0
--- /dev/null
+++ b/src/video.c
@@ -0,0 +1,261 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * vfetch - neofetch but the ascii part is a video
+ *
+ * Copyright (C) 2026 Doan Luu Duc Tai
+ *
+ */
+
+#include "video.h"
+#include <chafa/chafa.h>
+#include <glib.h>
+#include <sys/ioctl.h>
+#include <math.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <unistd.h>
+#include "pl_mpeg.h"
+#include "utils.h"
+
+static void apply_pixel_mode(ChafaCanvasConfig *config,
+ const VfetchArgs *args) {
+ if (strcmp(args->format_arg, "symbols") == 0) {
+ chafa_canvas_config_set_pixel_mode(config, CHAFA_PIXEL_MODE_SYMBOLS);
+ } else if (strcmp(args->format_arg, "sixels") == 0) {
+ chafa_canvas_config_set_pixel_mode(config, CHAFA_PIXEL_MODE_SIXELS);
+ } else if (strcmp(args->format_arg, "kitty") == 0) {
+ chafa_canvas_config_set_pixel_mode(config, CHAFA_PIXEL_MODE_KITTY);
+ } else if (strcmp(args->format_arg, "iterm2") == 0) {
+ chafa_canvas_config_set_pixel_mode(config, CHAFA_PIXEL_MODE_ITERM2);
+ }
+}
+
+static void apply_color_settings(ChafaCanvasConfig *config,
+ const VfetchArgs *args) {
+ if (args->colors_arg) {
+ if (strcmp(args->colors_arg, "16") == 0) {
+ chafa_canvas_config_set_canvas_mode(config, CHAFA_CANVAS_MODE_INDEXED_16);
+ } else if (strcmp(args->colors_arg, "240") == 0) {
+ chafa_canvas_config_set_canvas_mode(config,
+ CHAFA_CANVAS_MODE_INDEXED_240);
+ } else if (strcmp(args->colors_arg, "256") == 0) {
+ chafa_canvas_config_set_canvas_mode(config,
+ CHAFA_CANVAS_MODE_INDEXED_256);
+ } else if (strcmp(args->colors_arg, "full") == 0) {
+ chafa_canvas_config_set_canvas_mode(config, CHAFA_CANVAS_MODE_TRUECOLOR);
+ }
+ }
+ if (args->fg_color_arg) {
+ chafa_canvas_config_set_fg_color(config,
+ parse_hex_color(args->fg_color_arg));
+ }
+ if (args->bg_color_arg) {
+ chafa_canvas_config_set_bg_color(config,
+ parse_hex_color(args->bg_color_arg));
+ }
+}
+
+#define K_FALLBACK_CELL_WIDTH 10.0
+static VfetchError configure_chafa_canvas(uint32_t width,
+ uint32_t height,
+ ChafaCanvas **canvas_out,
+ const VfetchArgs *args) {
+ ChafaCanvasConfig *config = chafa_canvas_config_new();
+ chafa_canvas_config_set_geometry(config, (int)width, (int)height);
+
+ struct winsize window_size = {0};
+ int cell_px_w = 0;
+ int cell_px_h = 0;
+
+ if (isatty(STDOUT_FILENO) &&
+ ioctl(STDOUT_FILENO, TIOCGWINSZ, &window_size) == 0) {
+ if (window_size.ws_xpixel > 0 && window_size.ws_ypixel > 0 &&
+ window_size.ws_col > 0 && window_size.ws_row > 0) {
+ cell_px_w = window_size.ws_xpixel / window_size.ws_col;
+ cell_px_h = window_size.ws_ypixel / window_size.ws_row;
+ }
+ }
+
+ if (cell_px_w <= 0 || cell_px_h <= 0) {
+ cell_px_w = K_FALLBACK_CELL_WIDTH;
+ double h_val = round(K_FALLBACK_CELL_WIDTH / args->cell_aspect_ratio);
+ cell_px_h = (int)h_val;
+ }
+
+ chafa_canvas_config_set_cell_geometry(config, cell_px_w, cell_px_h);
+
+ apply_pixel_mode(config, args);
+ apply_color_settings(config, args);
+
+ ChafaSymbolMap *symbol_map = chafa_symbol_map_new();
+ char *symbols = (char *)g_hash_table_lookup(args->chafa_options, "--symbols");
+ chafa_symbol_map_apply_selectors(symbol_map, symbols ? symbols : "all", NULL);
+ chafa_canvas_config_set_symbol_map(config, symbol_map);
+ chafa_symbol_map_unref(symbol_map);
+
+ if (g_hash_table_lookup(args->chafa_options, "--fg-only")) {
+ chafa_canvas_config_set_fg_only_enabled(config, TRUE);
+ }
+
+ char *colorspace =
+ (char *)g_hash_table_lookup(args->chafa_options, "--color-space");
+ if (colorspace) {
+ if (strcmp(colorspace, "rgb") == 0) {
+ chafa_canvas_config_set_color_space(config, CHAFA_COLOR_SPACE_RGB);
+ } else if (strcmp(colorspace, "din99d") == 0) {
+ chafa_canvas_config_set_color_space(config, CHAFA_COLOR_SPACE_DIN99D);
+ }
+ }
+
+ char *fill = (char *)g_hash_table_lookup(args->chafa_options, "--fill");
+ if (fill && *fill) {
+ ChafaSymbolMap *fill_map = chafa_symbol_map_new();
+ for (guint i = 0; i < strlen(fill); i++) {
+ chafa_symbol_map_add_by_range(
+ fill_map, (gunichar)fill[i], (gunichar)fill[i]);
+ }
+ chafa_canvas_config_set_fill_symbol_map(config, fill_map);
+ chafa_symbol_map_unref(fill_map);
+ }
+
+ *canvas_out = chafa_canvas_new(config);
+ chafa_canvas_config_unref(config);
+
+ return (*canvas_out) ? VFETCH_SUCCESS : VFETCH_ERROR_CHAFA_CONFIG;
+}
+#undef K_FALLBACK_CELL_WIDTH
+
+static plm_t *probe_video(const char *filename) {
+ plm_t *plm = plm_create_with_filename(filename);
+ if (!plm) {
+ log_vfetch_error(VFETCH_ERROR_VIDEO_OPEN_FAILED, filename);
+ }
+ return plm;
+}
+
+VfetchError resolve_geometry(VfetchArgs *args) {
+ if (args->height > 0) {
+ return VFETCH_SUCCESS;
+ }
+
+ plm_t *plm = probe_video(args->filename);
+ if (!plm) {
+ return VFETCH_ERROR_VIDEO_OPEN_FAILED;
+ }
+
+ int v_w = plm_get_width(plm);
+ int v_h = plm_get_height(plm);
+ plm_destroy(plm);
+
+ double video_ratio = (double)v_h / (double)v_w;
+ double target_h = (double)args->width * args->cell_aspect_ratio * video_ratio;
+
+ double final_h = fmax(1.0, round(target_h));
+ args->height = (uint32_t)final_h;
+
+ return VFETCH_SUCCESS;
+}
+
+VfetchError get_video_frame_count(const char *filename,
+ double fps,
+ size_t *frame_count_out) {
+ plm_t *plm = probe_video(filename);
+ if (!plm) {
+ return VFETCH_ERROR_VIDEO_OPEN_FAILED;
+ }
+ double total_f = ceil(plm_get_duration(plm) * fps);
+ *frame_count_out = (size_t)total_f;
+ plm_destroy(plm);
+ return VFETCH_SUCCESS;
+}
+
+VfetchError validate_fps(VfetchArgs *args) {
+ plm_t *plm = probe_video(args->filename);
+ if (!plm) {
+ return VFETCH_ERROR_VIDEO_OPEN_FAILED;
+ }
+ if (args->fps <= 0.0) {
+ args->fps = plm_get_framerate(plm);
+ }
+ plm_destroy(plm);
+ return VFETCH_SUCCESS;
+}
+
+VfetchError initialize_worker_resources(WorkerResources *res,
+ const VfetchArgs *args) {
+ res->plm = plm_create_with_filename(args->filename);
+ if (!res->plm) {
+ return VFETCH_ERROR_FILE_IO;
+ }
+
+ res->video_width = plm_get_width(res->plm);
+ res->video_height = plm_get_height(res->plm);
+ res->rgb_buffer =
+ malloc((size_t)res->video_width * (size_t)res->video_height * 3);
+
+ return configure_chafa_canvas(args->width, args->height, &res->canvas, args);
+}
+
+void cleanup_worker_resources(WorkerResources *res) {
+ if (res->plm) {
+ plm_destroy(res->plm);
+ }
+ if (res->canvas) {
+ chafa_canvas_unref(res->canvas);
+ }
+ free(res->rgb_buffer);
+}
+
+static VfetchError cache_single_frame(WorkerResources *res,
+ plm_frame_t *frame,
+ CacheFiles *files,
+ unsigned long *current_offset) {
+ plm_frame_to_rgb(frame, res->rgb_buffer, res->video_width * 3);
+ chafa_canvas_draw_all_pixels(res->canvas,
+ CHAFA_PIXEL_RGB8,
+ res->rgb_buffer,
+ res->video_width,
+ res->video_height,
+ res->video_width * 3);
+
+ GString *gstr = chafa_canvas_print(res->canvas, NULL);
+ if (!gstr) {
+ return VFETCH_ERROR_MEMORY_ALLOCATION;
+ }
+
+ FrameMetadata meta = {
+ .offset = *current_offset, .length = gstr->len, .pts = frame->time};
+ VfetchError err = VFETCH_SUCCESS;
+
+ if (fwrite(gstr->str, 1, gstr->len, files->data_file) != gstr->len ||
+ fwrite(&meta, sizeof(FrameMetadata), 1, files->meta_file) != 1) {
+ err = VFETCH_ERROR_FILE_IO;
+ } else {
+ *current_offset += gstr->len;
+ }
+
+ g_string_free(gstr, TRUE);
+ return err;
+}
+
+VfetchError populate_frame_cache_loop(WorkerResources *res,
+ AppState *state,
+ CacheFiles *files,
+ unsigned long *current_offset) {
+ while (!atomic_load(&state->exit_requested)) {
+ plm_frame_t *frame = plm_decode_video(res->plm);
+ if (!frame) {
+ break;
+ }
+ if (cache_single_frame(res, frame, files, current_offset) !=
+ VFETCH_SUCCESS) {
+ return VFETCH_ERROR_FILE_IO;
+ }
+ atomic_fetch_add(&state->ascii_frames_completed, 1);
+ }
+ return VFETCH_SUCCESS;
+}