/* 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 #include #include #include #include #include #include #include #include #include #include #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; }