diff options
Diffstat (limited to 'src/video.c')
| -rw-r--r-- | src/video.c | 261 |
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; +} |
