/* 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 "core.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "args.h" #include "audio.h" #include "fetch.h" #include "pl_mpeg.h" #include "state.h" #include "utils.h" #include "video.h" #include "video_defs.h" static void free_cache_paths(CacheFilePaths *paths) { g_free(paths->data_file_path); g_free(paths->meta_file_path); } static VfetchError build_cache_paths(const char *cache_dir, CacheFilePaths *paths) { paths->data_file_path = g_build_filename(cache_dir, "frames.dat", NULL); paths->meta_file_path = g_build_filename(cache_dir, "frames.meta", NULL); if (!paths->data_file_path || !paths->meta_file_path) { log_vfetch_error(VFETCH_ERROR_MEMORY_ALLOCATION, "Failed to construct cache file paths."); free_cache_paths(paths); return VFETCH_ERROR_MEMORY_ALLOCATION; } return VFETCH_SUCCESS; } static VfetchError read_metadata_files(AnimationResources *res, const CacheFilePaths *paths) { res->meta_stream = fopen(paths->meta_file_path, "rb"); if (!res->meta_stream) { return VFETCH_ERROR_FILE_IO; } if (fseek(res->meta_stream, 0, SEEK_END) != 0) { return VFETCH_ERROR_FILE_IO; } long file_size = ftell(res->meta_stream); if (file_size < 0) { return VFETCH_ERROR_FILE_IO; } if (fseek(res->meta_stream, 0, SEEK_SET) != 0) { return VFETCH_ERROR_FILE_IO; } if ((size_t)file_size % sizeof(FrameMetadata) != 0) { return VFETCH_ERROR_CACHE_INVALID; } res->anim_args.num_frames = (size_t)file_size / sizeof(FrameMetadata); if (res->anim_args.num_frames == 0) { return VFETCH_ERROR_CACHE_INVALID; } res->anim_args.frames = malloc(res->anim_args.num_frames * sizeof(FrameMetadata)); if (!res->anim_args.frames) { return VFETCH_ERROR_MEMORY_ALLOCATION; } if (fread(res->anim_args.frames, sizeof(FrameMetadata), res->anim_args.num_frames, res->meta_stream) != res->anim_args.num_frames) { return VFETCH_ERROR_FILE_IO; } return VFETCH_SUCCESS; } static VfetchError map_frame_data_file(AnimationResources *res, const char *data_file_path) { GError *gerr = NULL; res->mapped_file = g_mapped_file_new(data_file_path, FALSE, &gerr); if (!res->mapped_file) { if (gerr) { (void)fprintf(stderr, "ERROR: Memory map failed: %s\n", gerr->message); g_error_free(gerr); } return VFETCH_ERROR_MMAP_FAILED; } res->anim_args.data_map = g_mapped_file_get_contents(res->mapped_file); res->data_file_size = (off_t)g_mapped_file_get_length(res->mapped_file); if (res->data_file_size <= 0) { g_mapped_file_unref(res->mapped_file); res->mapped_file = NULL; return VFETCH_ERROR_CACHE_INVALID; } return VFETCH_SUCCESS; } static bool validate_cache_file(const gchar *path, GStatBuf *st_out, bool check_non_empty) { if (g_stat(path, st_out) != 0) { return false; } if (check_non_empty && st_out->st_size == 0) { return false; } return true; } static gboolean validate_all_cache_files(const gchar *cache_dir_path, const CacheValidationStats *stats) { struct { const gchar *filename; GStatBuf *st_out; gboolean check_non_empty; } validation_rules[] = { {.filename = "_SUCCESS", .st_out = stats->st_success, .check_non_empty = FALSE}, {.filename = "frames.dat", .st_out = stats->st_data, .check_non_empty = TRUE}, {.filename = "frames.meta", .st_out = stats->st_meta, .check_non_empty = TRUE}, }; gchar *file_path = NULL; for (size_t idx = 0; idx < sizeof(validation_rules) / sizeof(validation_rules[0]); ++idx) { file_path = g_build_filename(cache_dir_path, validation_rules[idx].filename, NULL); if (!validate_cache_file(file_path, validation_rules[idx].st_out, validation_rules[idx].check_non_empty != FALSE)) { g_free(file_path); return FALSE; } g_free(file_path); } return TRUE; } static size_t verify_cache_file_integrity(const char *meta_path, const GStatBuf *st_data, const VfetchArgs *args) { FILE *meta_fp = fopen(meta_path, "rb"); if (!meta_fp) { return 0; } if (fseek(meta_fp, 0, SEEK_END) != 0) { (void)fclose(meta_fp); return 0; } long meta_size = ftell(meta_fp); if (meta_size <= 0 || (size_t)meta_size % sizeof(FrameMetadata) != 0) { log_debug( args->debug, "Cache invalid: Meta file size is not a multiple of a frame entry."); (void)fclose(meta_fp); return 0; } long last_entry_pos = meta_size - (long)sizeof(FrameMetadata); if (fseek(meta_fp, last_entry_pos, SEEK_SET) != 0) { (void)fclose(meta_fp); return 0; } FrameMetadata last_frame = {0}; if (fread(&last_frame, sizeof(FrameMetadata), 1, meta_fp) != 1) { (void)fclose(meta_fp); return 0; } (void)fclose(meta_fp); uint64_t expected_data_size = last_frame.offset + last_frame.length; if ((uint64_t)st_data->st_size != expected_data_size) { log_debug(args->debug, "Cache corrupted: Data file size mismatch. Expected %" PRIu64 ", got %" PRIu64, expected_data_size, (uint64_t)st_data->st_size); return 0; } return (size_t)meta_size / sizeof(FrameMetadata); } static VfetchError perform_cache_validation(AppState *state, const gchar *cache_dir_path, const VfetchArgs *args, const CacheValidationStats *stats, uint32_t *num_frames_out) { if (!validate_all_cache_files(cache_dir_path, stats)) { return VFETCH_ERROR_CACHE_INVALID; } char *meta_path = g_build_filename(cache_dir_path, "frames.meta", NULL); if (!meta_path) { return VFETCH_ERROR_MEMORY_ALLOCATION; } size_t frame_count = verify_cache_file_integrity(meta_path, stats->st_data, args); free(meta_path); if (frame_count == 0) { return VFETCH_ERROR_CACHE_INVALID; } if (frame_count > INT_MAX) { log_verbose(state, args->verbose, args->debug, "Error: Frame count (%zu) exceeds INT_MAX.", frame_count); return VFETCH_ERROR_CACHE_INVALID; } log_verbose(state, args->verbose, args->debug, "Cache is valid with %zu frames.", frame_count); *num_frames_out = (uint32_t)frame_count; return VFETCH_SUCCESS; } static VfetchError is_cache_valid(AppState *state, const gchar *cache_dir_path, const VfetchArgs *args, uint32_t *num_frames) { VfetchError err = VFETCH_ERROR_CACHE_INVALID; *num_frames = 0; CacheValidationStats *stats = malloc(sizeof(CacheValidationStats)); if (!stats) { return VFETCH_ERROR_MEMORY_ALLOCATION; } stats->st_success = g_new0(GStatBuf, 1); stats->st_data = g_new0(GStatBuf, 1); stats->st_meta = g_new0(GStatBuf, 1); if (!stats->st_success || !stats->st_data || !stats->st_meta) { err = VFETCH_ERROR_MEMORY_ALLOCATION; } else { err = perform_cache_validation( state, cache_dir_path, args, stats, num_frames); } free(stats->st_success); free(stats->st_data); free(stats->st_meta); free(stats); return err; } static VfetchError setup_cache_directory(AppState *state, const VfetchArgs *args) { GStatBuf file_stat = {0}; gchar *base_cache_dir = NULL; gchar *video_filename = NULL; gchar *video_cache_dirname = NULL; base_cache_dir = g_build_filename(g_get_user_cache_dir(), "vfetch", NULL); if (g_mkdir_with_parents(base_cache_dir, S_IRWXU) != 0) { log_vfetch_error(VFETCH_ERROR_MKDIR_FAILED, base_cache_dir); g_free(base_cache_dir); return VFETCH_ERROR_MKDIR_FAILED; } if (g_stat(args->filename, &file_stat) != 0) { log_vfetch_error(VFETCH_ERROR_STAT_FAILED, args->filename); g_free(base_cache_dir); return VFETCH_ERROR_STAT_FAILED; } video_filename = g_path_get_basename(args->filename); video_cache_dirname = g_strdup_printf("%s_dev%" G_GINT64_FORMAT "_ino%" G_GINT64_FORMAT, video_filename, (gint64)file_stat.st_dev, (gint64)file_stat.st_ino); state->video_specific_cache_root = g_build_filename(base_cache_dir, video_cache_dirname, NULL); g_free(base_cache_dir); g_free(video_filename); g_free(video_cache_dirname); if (!state->video_specific_cache_root) { return VFETCH_ERROR_MEMORY_ALLOCATION; } return VFETCH_SUCCESS; } #define VFETCH_CACHE_VERSION 1 static VfetchError build_cache_key(const VfetchArgs *args, CacheKey *key) { (void)memset(key, 0, sizeof(CacheKey)); GStatBuf file_stat = {0}; if (g_stat(args->filename, &file_stat) != 0) { return VFETCH_ERROR_STAT_FAILED; } key->file_size = file_stat.st_size; key->file_mtime = file_stat.st_mtime; key->width = args->width; key->height = args->height; key->fps = args->fps; key->cell_aspect_ratio = args->cell_aspect_ratio; key->version_tag = VFETCH_CACHE_VERSION; hash_string_to_bytes(args->format_arg, key->format_md5); hash_string_to_bytes(args->colors_arg, key->colors_md5); char *symbols = g_hash_table_lookup(args->chafa_options, "--symbols"); hash_string_to_bytes(symbols ? symbols : "all", key->symbols_md5); char *serialized_opts = serialize_map_for_hashing(args->chafa_options); hash_string_to_bytes(serialized_opts, key->chafa_opts_md5); g_free(serialized_opts); return VFETCH_SUCCESS; } #undef VFETCH_CACHE_VERSION static VfetchError generate_cache_hash(AppState *state, VfetchArgs *args, gchar **out_hash_str) { CacheKey key = {0}; VfetchError err = build_cache_key(args, &key); if (err != VFETCH_SUCCESS) { return err; } *out_hash_str = hash_cache_key(&key); state->current_args_cache_dir = g_build_filename(state->video_specific_cache_root, *out_hash_str, NULL); return VFETCH_SUCCESS; } static VfetchError initialize_pipeline_resources(const VfetchArgs *args, size_t *total_frames_out) { VfetchError err = get_video_frame_count(args->filename, args->fps, total_frames_out); if (err) { log_vfetch_error(err, args->filename); return err; } if (*total_frames_out == 0) { log_vfetch_error(VFETCH_ERROR_AV_STREAM_INFO, "Video has zero or negative duration"); return VFETCH_ERROR_AV_STREAM_INFO; } return VFETCH_SUCCESS; } static VfetchError open_cache_files_for_writing(CacheFiles *files, const gchar *cache_dir, gchar **data_path, gchar **meta_path) { *data_path = g_build_filename(cache_dir, "frames.dat", NULL); *meta_path = g_build_filename(cache_dir, "frames.meta", NULL); if (!*data_path || !*meta_path) { return VFETCH_ERROR_MEMORY_ALLOCATION; } files->data_file = fopen(*data_path, "wb"); files->meta_file = fopen(*meta_path, "wb"); if (!files->data_file || !files->meta_file) { return VFETCH_ERROR_FILE_IO; } return VFETCH_SUCCESS; } static VfetchError generate_ascii_cache(AppState *state, const VfetchArgs *args) { gchar *data_file_path = NULL; gchar *meta_file_path = NULL; WorkerResources res = {0}; CacheFiles files = {0}; unsigned long current_offset = 0; VfetchError err = VFETCH_SUCCESS; err = initialize_worker_resources(&res, args); if (err != VFETCH_SUCCESS) { cleanup_worker_resources(&res); return err; } err = open_cache_files_for_writing( &files, state->current_args_cache_dir, &data_file_path, &meta_file_path); if (err == VFETCH_SUCCESS) { err = populate_frame_cache_loop(&res, state, &files, ¤t_offset); } if (files.data_file) { (void)fclose(files.data_file); } if (files.meta_file) { (void)fclose(files.meta_file); } g_free(data_file_path); g_free(meta_file_path); cleanup_worker_resources(&res); return err; } static VfetchError run_processing_pipeline(AppState *state, const VfetchArgs *args, size_t total_frames) { ProgressThreadArgs *progress_args = NULL; pthread_t progress_thread_id = 0; VfetchError err = VFETCH_SUCCESS; progress_args = malloc(sizeof(ProgressThreadArgs)); if (!progress_args) { log_vfetch_error(VFETCH_ERROR_MEMORY_ALLOCATION, "progress thread args"); return VFETCH_ERROR_MEMORY_ALLOCATION; } progress_args->state = state; progress_args->total_frames = total_frames; if (pthread_create(&progress_thread_id, NULL, progress_display_thread_func, progress_args) != 0) { log_vfetch_error(VFETCH_ERROR_THREAD_CREATION, "progress display thread"); free(progress_args); return VFETCH_ERROR_THREAD_CREATION; } err = generate_ascii_cache(state, args); atomic_store_explicit( &state->processing_finished, true, memory_order_release); if (progress_thread_id != 0) { (void)pthread_join(progress_thread_id, NULL); } return err; } static VfetchError finalize_cache(const AppState *state) { GStatBuf st_dat = {0}; VfetchError err = VFETCH_SUCCESS; gchar *data_file_path = g_build_filename(state->current_args_cache_dir, "frames.dat", NULL); if (g_stat(data_file_path, &st_dat) != 0 && state->ascii_frames_completed > 0) { log_vfetch_error(VFETCH_ERROR_CACHE_INVALID, "Could not stat data file, but frames were written"); g_free(data_file_path); return VFETCH_ERROR_CACHE_INVALID; } g_free(data_file_path); char *success_file_path = g_build_filename(state->current_args_cache_dir, "_SUCCESS", NULL); FILE *success_file = fopen(success_file_path, "w"); if (success_file) { (void)fclose(success_file); } else { err = VFETCH_ERROR_FILE_IO; log_vfetch_error(err, success_file_path); } g_free(success_file_path); return err; } static VfetchError handle_cache_regeneration(AppState *state, VfetchArgs *args) { size_t total_frames = 0; if (!rm_rf(state->current_args_cache_dir)) { die(VFETCH_ERROR_FILE_IO, "Failed to clear cache directory"); } if (g_mkdir_with_parents(state->current_args_cache_dir, S_IRWXU) != 0) { log_vfetch_error(VFETCH_ERROR_MKDIR_FAILED, state->current_args_cache_dir); return VFETCH_ERROR_MKDIR_FAILED; } VfetchError err = initialize_pipeline_resources(args, &total_frames); if (err) { return err; } err = run_processing_pipeline(state, args, total_frames); if (atomic_load_explicit(&state->exit_requested, memory_order_acquire)) { (void)fputs("\nCaching cancelled. Cleaning up partial cache.\n", stderr); (void)rm_rf(state->current_args_cache_dir); return VFETCH_SUCCESS_EXIT_EARLY; } if (err != VFETCH_SUCCESS) { (void)fputs("\nPipeline encountered a technical error. Cleaning up partial " "cache.\n", stderr); (void)rm_rf(state->current_args_cache_dir); return err; } args->num_frames = (uint32_t)atomic_load_explicit( &state->ascii_frames_completed, memory_order_relaxed); log_verbose(state, args->verbose, args->debug, "Cache finalized with %u frames.", args->num_frames); return finalize_cache(state); } static VfetchError prepare_animation_assets(AppState *state, VfetchArgs *args, bool *cache_was_newly_created) { gchar *current_args_hash_str = NULL; uint32_t num_frames_from_cache = 0; VfetchError err = validate_fps(args); if (err != VFETCH_SUCCESS) { return err; } err = setup_cache_directory(state, args); if (err != VFETCH_SUCCESS) { return err; } err = generate_cache_hash(state, args, ¤t_args_hash_str); if (err != VFETCH_SUCCESS) { return err; } *cache_was_newly_created = false; if (is_cache_valid( state, state->current_args_cache_dir, args, &num_frames_from_cache) == VFETCH_SUCCESS) { args->num_frames = num_frames_from_cache; log_verbose(state, args->verbose, args->debug, "Loaded settings from a valid and integral cache."); g_free(current_args_hash_str); return VFETCH_SUCCESS; } *cache_was_newly_created = true; log_verbose(state, args->verbose, args->debug, "Cache invalid or not found, re-rendering."); err = handle_cache_regeneration(state, args); g_free(current_args_hash_str); return err; } static void cleanup_animation_resources(AnimationResources *res) { if (res->mapped_file) { g_mapped_file_unref(res->mapped_file); res->mapped_file = NULL; res->anim_args.data_map = NULL; } if (res->meta_stream) { (void)fclose(res->meta_stream); res->meta_stream = NULL; } free(res->anim_args.frames); res->anim_args.frames = NULL; free(res->anim_args.screen_buffer); res->anim_args.screen_buffer = NULL; } static VfetchError load_animation_data(const gchar *cache_dir, AnimationResources *res) { CacheFilePaths paths = {0}; VfetchError err = VFETCH_SUCCESS; if (build_cache_paths(cache_dir, &paths) != VFETCH_SUCCESS) { return VFETCH_ERROR_MEMORY_ALLOCATION; } err = read_metadata_files(res, &paths); if (err == VFETCH_SUCCESS) { err = map_frame_data_file(res, paths.data_file_path); } free_cache_paths(&paths); return err; } static VfetchError handle_animation_events(AppState *state, size_t *last_displayed_idx_ptr) { const char k_clear_command[] = "\x1b[H\x1b[2J\x1b[3J"; if (atomic_load_explicit(&state->terminal_resized, memory_order_acquire)) { if (write(STDOUT_FILENO, k_clear_command, sizeof(k_clear_command) - 1) < 0) { return VFETCH_ERROR_FILE_IO; } atomic_store_explicit( &state->terminal_resized, false, memory_order_release); *last_displayed_idx_ptr = (size_t)-1; } return VFETCH_SUCCESS; } static void handle_input_char(AppState *state, char char_typed) { if (char_typed == 'q' || char_typed == 'Q') { atomic_store_explicit(&state->exit_requested, true, memory_order_release); } else if (char_typed == 'p' || char_typed == 'P') { bool current = atomic_load_explicit(&state->paused, memory_order_acquire); atomic_store_explicit(&state->paused, (bool)!current, memory_order_release); } } static long perform_blocking_pause(AppState *state, struct pollfd *pfd) { gint64 p_start = 0; gint64 p_end = 0; (void)pthread_mutex_lock(&state->audio_state_mutex); if (state->audio_state.device.pContext) { ma_device_stop(&state->audio_state.device); } (void)pthread_mutex_unlock(&state->audio_state_mutex); p_start = g_get_monotonic_time(); while (atomic_load_explicit(&state->paused, memory_order_acquire) && !atomic_load_explicit(&state->exit_requested, memory_order_acquire)) { if (poll(pfd, 1, -1) > 0) { char char_typed = 0; if (pfd->revents & POLLIN && read(STDIN_FILENO, &char_typed, 1) > 0) { handle_input_char(state, char_typed); } } } p_end = g_get_monotonic_time(); (void)pthread_mutex_lock(&state->audio_state_mutex); if (state->audio_state.device.pContext && !atomic_load_explicit(&state->exit_requested, memory_order_acquire)) { ma_device_start(&state->audio_state.device); } (void)pthread_mutex_unlock(&state->audio_state_mutex); return p_end - p_start; } #define K_MICROSECONDS_IN_MILLISECOND 1000L static long wait_for_input(AppState *state, long delay_us) { struct pollfd pfd = {0}; pfd.fd = STDIN_FILENO; pfd.events = POLLIN; int timeout_ms = (int)(delay_us / K_MICROSECONDS_IN_MILLISECOND); if (timeout_ms < 0) { timeout_ms = 0; } if (poll(&pfd, 1, timeout_ms) > 0) { char char_typed = 0; if (pfd.revents & POLLIN && read(STDIN_FILENO, &char_typed, 1) > 0) { handle_input_char(state, char_typed); } } if (atomic_load_explicit(&state->paused, memory_order_acquire)) { return perform_blocking_pause(state, &pfd); } return 0; } #undef K_MICROSECONDS_IN_MILLISECOND static VfetchError render_frame(size_t frame_idx, size_t *last_displayed_idx_ptr, VideoAnimationArgs *anim_args, AppState *state, const VfetchArgs *args) { const char ansi_hide_cursor[] = "\x1b[?25l"; size_t final_len = 0; if (anim_args->num_frames == 0 || frame_idx >= anim_args->num_frames) { log_vfetch_error(VFETCH_ERROR_INVALID_ARGUMENT, "Attempted to render an out-of-bounds frame index."); return VFETCH_ERROR_INVALID_ARGUMENT; } if (frame_idx == *last_displayed_idx_ptr) { return VFETCH_SUCCESS; } int written = snprintf( anim_args->screen_buffer, anim_args->screen_buffer_size, "\x1b[H"); if (written > 0) { final_len = (size_t)written; } FrameMetadata current_frame = anim_args->frames[frame_idx]; if (current_frame.offset + current_frame.length > (uint64_t)anim_args->data_file_size) { log_vfetch_error(VFETCH_ERROR_CACHE_INVALID, "Frame index data is out of bounds."); return VFETCH_ERROR_CACHE_INVALID; } const char *frame_data_ptr = anim_args->data_map + current_frame.offset; append_block_to_buffer(anim_args->screen_buffer, anim_args->screen_buffer_size, &final_len, args->pad_x, args->pad_y, frame_data_ptr, current_frame.length); render_fetch_panel(anim_args->screen_buffer, anim_args->screen_buffer_size, &final_len, state, args); if (final_len + sizeof(ansi_hide_cursor) < anim_args->screen_buffer_size) { (void)snprintf(anim_args->screen_buffer + final_len, anim_args->screen_buffer_size - final_len, "%s", ansi_hide_cursor); } (void)pthread_mutex_lock(&state->stdout_mutex); ssize_t bytes_written = write(STDOUT_FILENO, anim_args->screen_buffer, strlen(anim_args->screen_buffer)); (void)pthread_mutex_unlock(&state->stdout_mutex); if (bytes_written < 0 && errno != EINTR) { return VFETCH_ERROR_FILE_IO; } *last_displayed_idx_ptr = frame_idx; return VFETCH_SUCCESS; } #define K_MICROSECONDS_IN_SECOND 1000000L static VfetchError run_continuous_animation_loop(VideoAnimationArgs *anim_args, AppState *state, const VfetchArgs *args) { const char k_clear_command[] = "\x1b[H\x1b[2J\x1b[3J"; VfetchError err = VFETCH_SUCCESS; gint64 start_time = 0; size_t last_displayed_idx = (size_t)-1; if (anim_args->num_frames == 0) { return VFETCH_SUCCESS; } double total_duration = anim_args->frames[anim_args->num_frames - 1].pts; start_time = g_get_monotonic_time(); size_t target_frame_idx = 0; if (write(STDOUT_FILENO, k_clear_command, sizeof(k_clear_command) - 1) < 0) { return VFETCH_ERROR_FILE_IO; } for (;;) { if (atomic_load_explicit(&state->exit_requested, memory_order_acquire)) { break; } err = handle_animation_events(state, &last_displayed_idx); if (err) { break; } gint64 current_time = g_get_monotonic_time(); double master_media_clock = (double)(current_time - start_time) / K_MICROSECONDS_IN_SECOND; if (master_media_clock >= total_duration) { if (args->loop) { start_time = current_time; target_frame_idx = 0; last_displayed_idx = (size_t)-1; master_media_clock = 0.0; } else { break; } } while (target_frame_idx < anim_args->num_frames - 1 && anim_args->frames[target_frame_idx + 1].pts < master_media_clock) { target_frame_idx++; } err = render_frame( target_frame_idx, &last_displayed_idx, anim_args, state, args); if (err) { atomic_store_explicit(&state->exit_requested, true, memory_order_release); break; } long refresh_delay_us = K_MICROSECONDS_IN_SECOND / args->refresh_rate; long paused_duration = wait_for_input(state, refresh_delay_us); if (paused_duration > 0) { start_time += paused_duration; } } return err; } #undef K_MICROSECONDS_IN_SECOND static void update_frame_index_from_clock(size_t *current_frame_idx, double master_media_clock, const VideoAnimationArgs *anim_args, AppState *state) { if (anim_args->num_frames == 0) { return; } double total_duration = anim_args->frames[anim_args->num_frames - 1].pts; double current_loop_clock = fmod(master_media_clock, total_duration); if (current_loop_clock < anim_args->frames[*current_frame_idx].pts) { *current_frame_idx = 0; } while (*current_frame_idx < anim_args->num_frames - 1 && anim_args->frames[*current_frame_idx + 1].pts <= current_loop_clock) { if (atomic_load_explicit(&state->exit_requested, memory_order_acquire)) { break; } (*current_frame_idx)++; } } #define K_MICROSECONDS_IN_SECOND 1000000.0 #define K_MAX_SYNC_DRIFT_SECONDS 2.0 static double calculate_media_clock_for_sync(AppState *state) { const double sample_rate = (double)state->audio_state.device.playback.internalSampleRate; const double audio_latency = state->audio_state.output_latency_seconds; uint64_t frames_consumed = atomic_load_explicit( &state->audio_state.frames_consumed, memory_order_acquire); double consumption_clock = (sample_rate > 0) ? (double)frames_consumed / sample_rate : 0.0; gint64 current_wall_time = g_get_monotonic_time(); if (state->media_sync_start_time == 0) { state->media_sync_start_time = current_wall_time - (gint64)((consumption_clock - audio_latency) * K_MICROSECONDS_IN_SECOND); } double wall_clock = (double)(current_wall_time - state->media_sync_start_time) / K_MICROSECONDS_IN_SECOND; double max_allowed = consumption_clock; double min_allowed = consumption_clock - K_MAX_SYNC_DRIFT_SECONDS; if (min_allowed < 0.0) { min_allowed = 0.0; } if (wall_clock > max_allowed) { wall_clock = max_allowed; state->media_sync_start_time = current_wall_time - (gint64)(wall_clock * K_MICROSECONDS_IN_SECOND); } else if (wall_clock < min_allowed) { wall_clock = min_allowed; state->media_sync_start_time = current_wall_time - (gint64)(wall_clock * K_MICROSECONDS_IN_SECOND); } return wall_clock; } #undef K_MICROSECONDS_IN_SECOND #undef K_MAX_SYNC_DRIFT_SECONDS #define K_MICROSECONDS_IN_SECOND 1000000L static VfetchError run_animation_with_sync(VideoAnimationArgs *anim_args, AppState *state, const VfetchArgs *args) { size_t last_displayed_idx = (size_t)-1; const char k_clear_command[] = "\x1b[H\x1b[2J\x1b[3J"; VfetchError err = VFETCH_SUCCESS; if (anim_args->num_frames == 0) { return VFETCH_SUCCESS; } double total_duration = anim_args->frames[anim_args->num_frames - 1].pts; size_t current_frame_idx = 0; if (write(STDOUT_FILENO, k_clear_command, sizeof(k_clear_command) - 1) < 0) { return VFETCH_ERROR_FILE_IO; } for (;;) { if (atomic_load_explicit(&state->exit_requested, memory_order_acquire)) { break; } err = handle_animation_events(state, &last_displayed_idx); if (err) { break; } double master_media_clock = calculate_media_clock_for_sync(state); if (master_media_clock >= total_duration) { if (args->loop) { (void)pthread_mutex_lock(&state->audio_state_mutex); plm_rewind(state->audio_state.plm); atomic_store_explicit( &state->audio_state.frames_consumed, 0, memory_order_release); (void)pthread_mutex_unlock(&state->audio_state_mutex); current_frame_idx = 0; last_displayed_idx = (size_t)-1; continue; } break; } update_frame_index_from_clock( ¤t_frame_idx, master_media_clock, anim_args, state); if (current_frame_idx != last_displayed_idx) { err = render_frame( current_frame_idx, &last_displayed_idx, anim_args, state, args); if (err) { break; } } long sleep_us = K_MICROSECONDS_IN_SECOND / args->refresh_rate; wait_for_input(state, sleep_us); } return err; } #undef K_MICROSECONDS_IN_SECOND static VfetchError run_animation_extract_audio(AppState *state, const VfetchArgs *args, VideoAnimationArgs *anim_args) { VfetchError err = VFETCH_SUCCESS; const char *audio_source = args->filename; err = initialize_audio(state, audio_source); if (err) { bool was_interrupted = atomic_load_explicit(&state->exit_requested, memory_order_acquire); if (was_interrupted) { shutdown_audio(state); return VFETCH_SUCCESS; } log_vfetch_error(err, "Could not initialize audio from video file."); return err; } err = run_animation_with_sync(anim_args, state, args); shutdown_audio(state); return err; } static VfetchError select_and_run_animation(AppState *state, const VfetchArgs *args, VideoAnimationArgs *anim_args) { if (args->audio) { return run_animation_extract_audio(state, args, anim_args); } return run_continuous_animation_loop(anim_args, state, args); } #define K_SCREEN_BUFFER_SIZE (2 * 1024 * 1024) static VfetchError run_animation_loop(AppState *state, const VfetchArgs *args) { atomic_store_explicit( &state->animation_has_started, true, memory_order_release); AnimationResources res = {0}; res.mapped_file = NULL; VfetchError err = load_animation_data(state->current_args_cache_dir, &res); if (err) { cleanup_animation_resources(&res); log_vfetch_error(err, "Failed to load animation data from cache"); return err; } res.anim_args.data_file_size = res.data_file_size; res.anim_args.screen_buffer_size = (long)K_SCREEN_BUFFER_SIZE; res.anim_args.screen_buffer = malloc(res.anim_args.screen_buffer_size); if (!res.anim_args.screen_buffer) { err = VFETCH_ERROR_MEMORY_ALLOCATION; log_vfetch_error(err, "Failed to allocate screen buffer."); cleanup_animation_resources(&res); return err; } err = select_and_run_animation(state, args, &res.anim_args); cleanup_animation_resources(&res); return err; } #undef K_SCREEN_BUFFER_SIZE static VfetchError initialize_paths_and_geometry(VfetchArgs *args) { gchar *abs_path = g_canonicalize_filename(args->filename, NULL); if (abs_path == NULL) { return VFETCH_ERROR_PATH_FAILED; } g_free(args->filename); args->filename = abs_path; VfetchError err = resolve_geometry(args); if (err) { return err; } return VFETCH_SUCCESS; } static VfetchError initialize_app_state(AppState *state) { global_signal_handler_state_ptr = state; if (pthread_mutex_init(&state->fetch_mutex, NULL) != 0 || pthread_mutex_init(&state->audio_state_mutex, NULL) != 0 || pthread_mutex_init(&state->stdout_mutex, NULL) != 0) { return VFETCH_ERROR_MUTEX_INIT; } atomic_store_explicit( &state->processing_finished, false, memory_order_release); return VFETCH_SUCCESS; } static VfetchError setup_environment(AppState *state) { struct sigaction sig_action = {0}; VfetchError err = VFETCH_SUCCESS; setup_terminal(state); set_cursor_visible(state, false); if (pipe(global_signal_pipe) == -1) { err = VFETCH_ERROR_PIPE_CREATION; log_vfetch_error(err, "Failed to create signal pipe."); return err; } (void)memset(&sig_action, 0, sizeof(sig_action)); sig_action.sa_flags = SA_SIGINFO; sig_action.sa_sigaction = app_signal_handler; (void)sigaction(SIGINT, &sig_action, NULL); (void)sigaction(SIGTERM, &sig_action, NULL); sig_action.sa_flags = SA_RESTART | SA_SIGINFO; sig_action.sa_sigaction = app_resize_handler; (void)sigaction(SIGWINCH, &sig_action, NULL); return err; } static VfetchError run_core_logic(AppState *state, VfetchArgs *args) { bool cache_was_newly_created = false; VfetchError err = initialize_paths_and_geometry(args); if (err) { log_vfetch_error(err, "Failed to initialize paths and geometry"); return err; } if (!args->fetch_tool_arg || strcmp(args->fetch_tool_arg, "none") != 0) { err = initialize_fetch_utility(state, args); if (err) { return err; } } err = prepare_animation_assets(state, args, &cache_was_newly_created); if (err == VFETCH_SUCCESS_EXIT_EARLY) { return VFETCH_SUCCESS; } if (err) { return err; } bool should_exit = atomic_load_explicit(&state->exit_requested, memory_order_acquire); if (should_exit) { (void)fputs("\nOperation cancelled during asset preparation.\n", stderr); err = VFETCH_SUCCESS; return err; } err = start_fetch_thread(state, args); if (err) { return err; } err = run_animation_loop(state, args); return err; } static void teardown_threads_and_pipes(AppState *state) { atomic_store_explicit(&state->exit_requested, true, memory_order_release); if (global_signal_pipe[1] != -1) { (void)close(global_signal_pipe[1]); global_signal_pipe[1] = -1; } if (state->signal_monitor_thread_id != 0) { (void)pthread_join(state->signal_monitor_thread_id, NULL); } if (global_signal_pipe[0] != -1) { (void)close(global_signal_pipe[0]); global_signal_pipe[0] = -1; } } static void cleanup_args_resources(VfetchArgs *args) { if (!args) { return; } g_free(args->filename); g_free(args->colors_arg); g_free(args->fg_color_arg); g_free(args->bg_color_arg); g_free(args->format_arg); g_free(args->fetch_tool_arg); if (args->chafa_options) { g_hash_table_destroy(args->chafa_options); } } VfetchError execute_program(int argc, const char *argv[], AppState *state, VfetchArgs *args) { VfetchError err = initialize_arguments(args); if (err) { return err; } err = parse_arguments(argc, argv, args); if (err) { return err; } err = initialize_app_state(state); if (err) { return err; } state->args = args; err = setup_environment(state); if (err) { return err; } if (pthread_create(&state->signal_monitor_thread_id, NULL, signal_monitor_thread_func, state) != 0) { err = VFETCH_ERROR_THREAD_CREATION; log_vfetch_error(err, "signal monitor thread"); return err; } err = run_core_logic(state, args); return err; } void cleanup_application(AppState *state, VfetchArgs *args) { if (state) { teardown_threads_and_pipes(state); restore_terminal(); if (state->audio_state.device.pContext != NULL) { shutdown_audio(state); } pthread_mutex_destroy(&state->fetch_mutex); pthread_mutex_destroy(&state->audio_state_mutex); g_free(state->video_specific_cache_root); g_free(state->current_args_cache_dir); g_free(state->live_fetch_output); g_free(state->fetch_command); } global_signal_handler_state_ptr = NULL; cleanup_args_resources(args); }