diff options
Diffstat (limited to 'src/core.c')
| -rw-r--r-- | src/core.c | 1272 |
1 files changed, 1272 insertions, 0 deletions
diff --git a/src/core.c b/src/core.c new file mode 100644 index 0000000..f4a53e7 --- /dev/null +++ b/src/core.c @@ -0,0 +1,1272 @@ +/* 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 <glib.h> +#include <glib/gstdio.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <errno.h> +#include <inttypes.h> +#include <limits.h> +#include <math.h> +#include <miniaudio.h> +#include <poll.h> +#include <pthread.h> +#include <signal.h> +#include <stdatomic.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#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); +} |
