summaryrefslogtreecommitdiff
path: root/src/core.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/core.c')
-rw-r--r--src/core.c1272
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, &current_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, &current_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(
+ &current_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);
+}