diff --git a/src/app.c b/src/app.c index 1d8357a..6ac3aef 100644 --- a/src/app.c +++ b/src/app.c @@ -74,9 +74,11 @@ static int update_video_texture(App *app) { FrameData frame = {0}; double sync_clock; double lead_tolerance; + Uint64 now_ticks; - sync_clock = player_get_sync_clock(&app->player, time(NULL)); - lead_tolerance = app->startup_handoff_active ? 0.220 : 0.035; + now_ticks = SDL_GetTicks64(); + sync_clock = player_get_sync_clock(&app->player, now_ticks); + lead_tolerance = app->startup_handoff_active ? 0.240 : 0.060; if (!player_consume_synced_frame(&app->player, &frame, sync_clock, lead_tolerance)) { if (!app->startup_handoff_active || !player_consume_latest_frame(&app->player, &frame)) { @@ -172,7 +174,6 @@ int app_init(App *app) { app->running = 1; app->mode = MODE_FULLSCREEN; app->last_blackout_state = 1; - app->app_start_time = time(NULL); configure_runtime_environment(); log_runtime_environment("startup-before-sdl"); @@ -188,6 +189,9 @@ int app_init(App *app) { return -1; } + app->app_start_time = time(NULL); + app->app_start_ticks = SDL_GetTicks64(); + if (TTF_Init() != 0) { fprintf(stderr, "SDL_ttf init failed: %s\n", TTF_GetError()); return -1; @@ -216,11 +220,16 @@ int app_init(App *app) { return -1; } - if (player_init(&app->player, &app->channels, app->app_start_time) != 0) { + if (player_init(&app->player, &app->channels, app->app_start_ticks) != 0) { fprintf(stderr, "Unable to initialize player.\n"); return -1; } + if (ui_cache_init(&app->ui_cache, app->renderer, &app->fonts, &app->channels) != 0) { + fprintf(stderr, "Unable to initialize UI cache.\n"); + return -1; + } + if (app->channels.count > 0) { begin_startup_handoff(app); player_tune(&app->player, 0); @@ -234,6 +243,8 @@ void app_run(App *app) { while (app->running) { int in_blackout; + Uint64 now_ticks; + time_t now_wall; while (SDL_PollEvent(&event)) { handle_event(app, &event); @@ -245,6 +256,8 @@ void app_run(App *app) { } in_blackout = player_is_in_blackout(&app->player); + now_ticks = SDL_GetTicks64(); + now_wall = time(NULL); if (app->last_blackout_state && !in_blackout) { app->startup_handoff_active = 1; app->startup_handoff_until = SDL_GetTicks() + 400; @@ -253,7 +266,7 @@ void app_run(App *app) { app->last_blackout_state = in_blackout; if (app->channels.count == 0) { - ui_render_no_media(app->renderer, &app->fonts); + ui_render_no_media(app->renderer, &app->ui_cache); } else if (app->mode == MODE_GUIDE) { if (!in_blackout) { player_resume_audio(&app->player); @@ -263,10 +276,12 @@ void app_run(App *app) { app->texture_width, app->texture_height, &app->fonts, + &app->ui_cache, &app->channels, app->player.current_index, - app->app_start_time, - time(NULL)); + app->app_start_ticks, + now_ticks, + now_wall); } else { if (!in_blackout) { player_resume_audio(&app->player); @@ -289,6 +304,7 @@ void app_destroy(App *app) { player_destroy(&app->player); channel_list_destroy(&app->channels); destroy_video_texture(app); + ui_cache_destroy(&app->ui_cache); ui_destroy_fonts(&app->fonts); if (app->renderer) { SDL_DestroyRenderer(app->renderer); diff --git a/src/app.h b/src/app.h index edfcdb9..1a70c95 100644 --- a/src/app.h +++ b/src/app.h @@ -20,9 +20,11 @@ typedef struct App { int last_blackout_state; Uint32 startup_handoff_until; time_t app_start_time; + Uint64 app_start_ticks; ChannelList channels; Player player; UiFonts fonts; + UiCache ui_cache; } App; int app_init(App *app); diff --git a/src/app.o b/src/app.o index d0b92bb..b89b500 100644 Binary files a/src/app.o and b/src/app.o differ diff --git a/src/channel.c b/src/channel.c index c4ef3a1..10a9916 100644 --- a/src/channel.c +++ b/src/channel.c @@ -189,3 +189,18 @@ double channel_live_position(const Channel *channel, time_t app_start_time, time return fmod(elapsed, channel->duration_seconds); } + +double channel_live_position_precise(const Channel *channel, Uint64 app_start_ticks, Uint64 now_ticks) { + double elapsed; + + if (!channel || channel->duration_seconds <= 0.0) { + return 0.0; + } + + if (now_ticks < app_start_ticks) { + now_ticks = app_start_ticks; + } + + elapsed = (double) (now_ticks - app_start_ticks) / 1000.0; + return fmod(elapsed, channel->duration_seconds); +} diff --git a/src/channel.h b/src/channel.h index 28fb1d4..fcb6354 100644 --- a/src/channel.h +++ b/src/channel.h @@ -1,6 +1,7 @@ #ifndef CHANNEL_H #define CHANNEL_H +#include #include #include @@ -21,5 +22,6 @@ typedef struct ChannelList { int channel_list_load(ChannelList *list, const char *media_dir); void channel_list_destroy(ChannelList *list); double channel_live_position(const Channel *channel, time_t app_start_time, time_t now); +double channel_live_position_precise(const Channel *channel, Uint64 app_start_ticks, Uint64 now_ticks); #endif diff --git a/src/channel.o b/src/channel.o index 542bcfa..619064d 100644 Binary files a/src/channel.o and b/src/channel.o differ diff --git a/src/frame_queue.h b/src/frame_queue.h index cfde523..d4cdf1e 100644 --- a/src/frame_queue.h +++ b/src/frame_queue.h @@ -3,7 +3,7 @@ #include -#define FRAME_QUEUE_CAPACITY 8 +#define FRAME_QUEUE_CAPACITY 12 typedef struct FrameData { unsigned char *buffer; diff --git a/src/player.c b/src/player.c index ed30848..a57f849 100644 --- a/src/player.c +++ b/src/player.c @@ -272,7 +272,7 @@ static int queue_audio_frame(Player *player, : audio_frame->best_effort_timestamp * av_q2d(audio_time_base); frame_duration = (double) sample_count / (double) out_rate; - queued_limit = (int) (player_audio_bytes_per_second(player) * 0.12); + queued_limit = (int) (player_audio_bytes_per_second(player) * 0.18); while (!should_stop(player) && (int) SDL_GetQueuedAudioSize(player->audio_device) > queued_limit) { SDL_Delay(2); } @@ -420,7 +420,7 @@ static int decode_thread_main(void *userdata) { goto cleanup; } - seek_seconds = channel_live_position(channel, player->app_start_time, time(NULL)); + seek_seconds = channel_live_position_precise(channel, player->app_start_ticks, SDL_GetTicks64()); if (channel->duration_seconds > 0.0) { int64_t seek_target = (int64_t) (seek_seconds / av_q2d(time_base)); avformat_seek_file(format_context, video_stream_index, INT64_MIN, seek_target, INT64_MAX, 0); @@ -583,7 +583,7 @@ static void player_stop_thread(Player *player) { player->thread = NULL; } -int player_init(Player *player, const ChannelList *channels, time_t app_start_time) { +int player_init(Player *player, const ChannelList *channels, Uint64 app_start_ticks) { if (!player || !channels) { return -1; } @@ -591,7 +591,7 @@ int player_init(Player *player, const ChannelList *channels, time_t app_start_ti memset(player, 0, sizeof(*player)); player->channels = channels; player->current_index = -1; - player->app_start_time = app_start_time; + player->app_start_ticks = app_start_ticks; player->audio_mutex = SDL_CreateMutex(); player->clock_mutex = SDL_CreateMutex(); player->error_mutex = SDL_CreateMutex(); @@ -692,7 +692,7 @@ int player_consume_synced_frame(Player *player, FrameData *out, double clock_sec return 0; } - late_tolerance = SDL_GetTicks() < player->catchup_until ? 0.180 : 0.090; + late_tolerance = SDL_GetTicks() < player->catchup_until ? 0.220 : 0.130; while (frame_queue_peek_first(&player->frame_queue, &candidate)) { if (candidate.pts_seconds < clock_seconds - late_tolerance) { @@ -728,15 +728,15 @@ int player_consume_synced_frame(Player *player, FrameData *out, double clock_sec return 0; } -double player_live_position(const Player *player, time_t now) { +double player_live_position(const Player *player, Uint64 now_ticks) { if (!player || !player->channels || player->current_index < 0 || player->current_index >= player->channels->count) { return 0.0; } - return channel_live_position(&player->channels->items[player->current_index], player->app_start_time, now); + return channel_live_position_precise(&player->channels->items[player->current_index], player->app_start_ticks, now_ticks); } -double player_get_sync_clock(Player *player, time_t now) { +double player_get_sync_clock(Player *player, Uint64 now_ticks) { double clock_seconds; double queued_seconds = 0.0; double bytes_per_second; @@ -747,12 +747,12 @@ double player_get_sync_clock(Player *player, time_t now) { } if (!player->has_audio_stream || player->audio_device == 0) { - return player_live_position(player, now); + return player_live_position(player, now_ticks); } bytes_per_second = player_audio_bytes_per_second(player); if (bytes_per_second <= 0.0) { - return player_live_position(player, now); + return player_live_position(player, now_ticks); } SDL_LockMutex(player->clock_mutex); @@ -761,7 +761,7 @@ double player_get_sync_clock(Player *player, time_t now) { SDL_UnlockMutex(player->clock_mutex); if (!audio_started) { - return player_live_position(player, now); + return player_live_position(player, now_ticks); } queued_seconds = (double) SDL_GetQueuedAudioSize(player->audio_device) / bytes_per_second; diff --git a/src/player.h b/src/player.h index 78b8ece..0568cf6 100644 --- a/src/player.h +++ b/src/player.h @@ -13,7 +13,7 @@ typedef struct Player { SDL_atomic_t stop_requested; const ChannelList *channels; int current_index; - time_t app_start_time; + Uint64 app_start_ticks; Uint32 tuning_blackout_until; SDL_AudioDeviceID audio_device; SDL_AudioSpec audio_spec; @@ -29,13 +29,13 @@ typedef struct Player { char last_error[256]; } Player; -int player_init(Player *player, const ChannelList *channels, time_t app_start_time); +int player_init(Player *player, const ChannelList *channels, Uint64 app_start_ticks); void player_destroy(Player *player); int player_tune(Player *player, int channel_index); int player_consume_latest_frame(Player *player, FrameData *out); int player_consume_synced_frame(Player *player, FrameData *out, double clock_seconds, double lead_tolerance); -double player_live_position(const Player *player, time_t now); -double player_get_sync_clock(Player *player, time_t now); +double player_live_position(const Player *player, Uint64 now_ticks); +double player_get_sync_clock(Player *player, Uint64 now_ticks); void player_set_catchup_until(Player *player, Uint32 tick); int player_is_in_blackout(const Player *player); void player_resume_audio(Player *player); diff --git a/src/player.o b/src/player.o index c9f8d2a..fd2c547 100644 Binary files a/src/player.o and b/src/player.o differ diff --git a/src/ui.c b/src/ui.c index 654be97..2aa6c32 100644 --- a/src/ui.c +++ b/src/ui.c @@ -12,6 +12,32 @@ static const char *FONT_CANDIDATES[] = { }; static void fill_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color); +static SDL_Texture *text_to_texture(SDL_Renderer *renderer, TTF_Font *font, const char *text, SDL_Color color, int *width, int *height); + +static void text_texture_destroy(UiTextTexture *text_texture) { + if (!text_texture) { + return; + } + + if (text_texture->texture) { + SDL_DestroyTexture(text_texture->texture); + } + memset(text_texture, 0, sizeof(*text_texture)); +} + +static int text_texture_init(UiTextTexture *text_texture, + SDL_Renderer *renderer, + TTF_Font *font, + const char *text, + SDL_Color color) { + if (!text_texture) { + return -1; + } + + memset(text_texture, 0, sizeof(*text_texture)); + text_texture->texture = text_to_texture(renderer, font, text, color, &text_texture->width, &text_texture->height); + return text_texture->texture ? 0 : -1; +} static void set_draw_color(SDL_Renderer *renderer, SDL_Color color) { SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); @@ -41,23 +67,18 @@ static SDL_Texture *text_to_texture(SDL_Renderer *renderer, TTF_Font *font, cons return texture; } -static void draw_text(SDL_Renderer *renderer, TTF_Font *font, const char *text, int x, int y, SDL_Color color) { - SDL_Texture *texture; +static void draw_cached_text(SDL_Renderer *renderer, const UiTextTexture *text_texture, int x, int y) { SDL_Rect dst; - int width = 0; - int height = 0; - texture = text_to_texture(renderer, font, text, color, &width, &height); - if (!texture) { + if (!text_texture || !text_texture->texture) { return; } dst.x = x; dst.y = y; - dst.w = width; - dst.h = height; - SDL_RenderCopy(renderer, texture, NULL, &dst); - SDL_DestroyTexture(texture); + dst.w = text_texture->width; + dst.h = text_texture->height; + SDL_RenderCopy(renderer, text_texture->texture, NULL, &dst); } static void fill_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color) { @@ -106,22 +127,20 @@ static void format_clock_label(char *buffer, size_t buffer_size, time_t now, int strftime(buffer, buffer_size, "%I:%M %p", &local_time); } -static void draw_timeline_header(SDL_Renderer *renderer, const UiFonts *fonts, SDL_Rect rect, time_t now) { - char label[32]; +static void draw_timeline_header_cached(SDL_Renderer *renderer, const UiCache *cache, SDL_Rect rect) { int segments = 4; - draw_text(renderer, fonts->medium, "FRI 6/30", rect.x + 12, rect.y + 8, COLOR_TEXT_DARK); + draw_cached_text(renderer, &cache->timeline_day, rect.x + 12, rect.y + 8); for (int i = 0; i < segments; ++i) { int x = rect.x + (rect.w * i) / segments; - format_clock_label(label, sizeof(label), now, (int) ((TIMELINE_VISIBLE_SECONDS / 60.0 / segments) * i)); - draw_text(renderer, fonts->small, label, x + 6, rect.y + 10, COLOR_TEXT_DARK); + draw_cached_text(renderer, &cache->timeline_labels[i], x + 6, rect.y + 10); set_draw_color(renderer, COLOR_SLATE); SDL_RenderDrawLine(renderer, x, rect.y + rect.h - 4, x, rect.y + rect.h + 5 * 76); } } -static void draw_footer_legend(SDL_Renderer *renderer, const UiFonts *fonts, int window_width, int window_height) { +static void draw_footer_legend(SDL_Renderer *renderer, const UiCache *cache, int window_width, int window_height) { SDL_Rect footer = {0, window_height - 54, window_width, 54}; SDL_Rect chip = {window_width / 2 - 170, window_height - 40, 24, 24}; @@ -130,26 +149,26 @@ static void draw_footer_legend(SDL_Renderer *renderer, const UiFonts *fonts, int SDL_RenderDrawLine(renderer, footer.x, footer.y, footer.x + footer.w, footer.y); fill_rect(renderer, &chip, COLOR_HIGHLIGHT_YELLOW); - draw_text(renderer, fonts->medium, "A", chip.x + 7, chip.y + 2, COLOR_TEXT_DARK); - draw_text(renderer, fonts->medium, "Time", chip.x + 34, chip.y + 1, COLOR_TEXT_DARK); + draw_cached_text(renderer, &cache->footer_a, chip.x + 7, chip.y + 2); + draw_cached_text(renderer, &cache->footer_time, chip.x + 34, chip.y + 1); chip.x += 132; fill_rect(renderer, &chip, COLOR_DEEP_BLUE); - draw_text(renderer, fonts->medium, "B", chip.x + 7, chip.y + 2, COLOR_TEXT_LIGHT); - draw_text(renderer, fonts->medium, "Theme", chip.x + 34, chip.y + 1, COLOR_TEXT_DARK); + draw_cached_text(renderer, &cache->footer_b, chip.x + 7, chip.y + 2); + draw_cached_text(renderer, &cache->footer_theme, chip.x + 34, chip.y + 1); chip.x += 136; fill_rect(renderer, &chip, COLOR_HINT_RED); - draw_text(renderer, fonts->medium, "C", chip.x + 7, chip.y + 2, COLOR_TEXT_LIGHT); - draw_text(renderer, fonts->medium, "Title", chip.x + 34, chip.y + 1, COLOR_TEXT_DARK); + draw_cached_text(renderer, &cache->footer_c, chip.x + 7, chip.y + 2); + draw_cached_text(renderer, &cache->footer_title, chip.x + 34, chip.y + 1); } -void ui_render_no_media(SDL_Renderer *renderer, const UiFonts *fonts) { +void ui_render_no_media(SDL_Renderer *renderer, const UiCache *cache) { SDL_Rect full = {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}; fill_rect(renderer, &full, COLOR_NAVY_DARK); - draw_text(renderer, fonts->large, "Passport-C Media Player", 58, 80, COLOR_TEXT_LIGHT); - draw_text(renderer, fonts->medium, "No channels found in ./media", 58, 136, COLOR_HIGHLIGHT_YELLOW); - draw_text(renderer, fonts->small, "Add MP4 or MKV files to ./media and relaunch.", 58, 176, COLOR_PALE_BLUE); + draw_cached_text(renderer, &cache->no_media_title, 58, 80); + draw_cached_text(renderer, &cache->no_media_body, 58, 136); + draw_cached_text(renderer, &cache->no_media_hint, 58, 176); } void ui_render_fullscreen(SDL_Renderer *renderer, SDL_Texture *video_texture, int texture_width, int texture_height) { @@ -163,10 +182,12 @@ void ui_render_guide(SDL_Renderer *renderer, int texture_width, int texture_height, const UiFonts *fonts, + UiCache *cache, const ChannelList *channels, int active_channel, - time_t app_start_time, - time_t now) { + Uint64 app_start_ticks, + Uint64 now_ticks, + time_t now_wall) { SDL_Rect full = {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}; SDL_Rect header_card = {0, 0, 390, 168}; SDL_Rect preview = {820, 0, 460, 210}; @@ -189,13 +210,22 @@ void ui_render_guide(SDL_Renderer *renderer, const Channel *current = &channels->items[active_channel]; snprintf(detail, sizeof(detail), "%.0f min - %s", current->duration_seconds / 60.0, current->file_name); fill_rect(renderer, &(SDL_Rect){0, 0, 390, 38}, COLOR_HIGHLIGHT_YELLOW); - draw_text(renderer, fonts->large, current->name, 26, 44, COLOR_TEXT_DARK); - draw_text(renderer, fonts->medium, current->program_title, 20, 92, COLOR_TEXT_DARK); - draw_text(renderer, fonts->small, detail, 20, 124, COLOR_TEXT_DARK); - draw_text(renderer, fonts->medium, "Nick * 56", preview.x + preview.w - 126, preview.y + preview.h - 34, COLOR_TEXT_LIGHT); + draw_cached_text(renderer, &cache->channels[active_channel].name_medium, 26, 52); + draw_cached_text(renderer, &cache->channels[active_channel].program_medium_dark, 20, 92); + draw_cached_text(renderer, &cache->channels[active_channel].detail_small_dark, 20, 124); + draw_cached_text(renderer, &cache->preview_badge, preview.x + preview.w - 126, preview.y + preview.h - 34); } - draw_timeline_header(renderer, fonts, header_row, now); + if (cache->timeline_label_slot != now_wall / 60) { + char label[32]; + for (int i = 0; i < 4; ++i) { + format_clock_label(label, sizeof(label), now_wall, (int) ((TIMELINE_VISIBLE_SECONDS / 60.0 / 4) * i)); + text_texture_destroy(&cache->timeline_labels[i]); + text_texture_init(&cache->timeline_labels[i], renderer, fonts->small, label, COLOR_TEXT_DARK); + } + cache->timeline_label_slot = now_wall / 60; + } + draw_timeline_header_cached(renderer, cache, header_row); if (start_index < 0) { start_index = 0; @@ -223,34 +253,132 @@ void ui_render_guide(SDL_Renderer *renderer, { const Channel *channel = &channels->items[channel_index]; - char number[16]; - double live_position = channel_live_position(channel, app_start_time, now); + double live_position = channel_live_position_precise(channel, app_start_ticks, now_ticks); double pixels_per_second = timeline_w / TIMELINE_VISIBLE_SECONDS; int block_x = timeline_x - (int) (live_position * pixels_per_second); int block_w = (int) (channel->duration_seconds * pixels_per_second); SDL_Rect block = {block_x, timeline_rect.y + 4, SDL_max(block_w, 48), timeline_rect.h - 8}; - snprintf(number, sizeof(number), "%d", channel->number); - draw_text(renderer, fonts->medium, channel->name, 20, row_rect.y + 12, COLOR_TEXT_LIGHT); - draw_text(renderer, fonts->medium, number, 176, row_rect.y + 12, COLOR_TEXT_LIGHT); - draw_text(renderer, fonts->small, channel->file_name, 20, row_rect.y + 38, COLOR_PALE_BLUE); + draw_cached_text(renderer, &cache->channels[channel_index].name_medium, 20, row_rect.y + 12); + draw_cached_text(renderer, &cache->channels[channel_index].number_medium, 176, row_rect.y + 12); + draw_cached_text(renderer, &cache->channels[channel_index].file_small, 20, row_rect.y + 38); fill_rect(renderer, &timeline_rect, COLOR_NAVY_DARK); SDL_RenderSetClipRect(renderer, &clip); fill_rect(renderer, &block, is_selected ? COLOR_HIGHLIGHT_YELLOW : COLOR_DEEP_BLUE); stroke_rect(renderer, &block, COLOR_PALE_BLUE); - draw_text(renderer, - is_selected ? fonts->medium : fonts->small, - channel->program_title, - block.x + 10, - block.y + 10, - is_selected ? COLOR_TEXT_DARK : COLOR_TEXT_LIGHT); + draw_cached_text(renderer, + is_selected ? &cache->channels[channel_index].program_medium_dark + : &cache->channels[channel_index].program_small_light, + block.x + 10, + block.y + 10); SDL_RenderSetClipRect(renderer, NULL); stroke_rect(renderer, &timeline_rect, COLOR_SLATE); } } - draw_footer_legend(renderer, fonts, WINDOW_WIDTH, WINDOW_HEIGHT); + draw_footer_legend(renderer, cache, WINDOW_WIDTH, WINDOW_HEIGHT); +} + +int ui_cache_init(UiCache *cache, SDL_Renderer *renderer, const UiFonts *fonts, const ChannelList *channels) { + char buffer[256]; + + if (!cache || !renderer || !fonts) { + return -1; + } + + memset(cache, 0, sizeof(*cache)); + cache->renderer = renderer; + cache->timeline_label_slot = -1; + cache->timeline_labels = calloc(4, sizeof(UiTextTexture)); + if (!cache->timeline_labels) { + ui_cache_destroy(cache); + return -1; + } + + if (text_texture_init(&cache->no_media_title, renderer, fonts->large, "Passport-C Media Player", COLOR_TEXT_LIGHT) != 0 || + text_texture_init(&cache->no_media_body, renderer, fonts->medium, "No channels found in ./media", COLOR_HIGHLIGHT_YELLOW) != 0 || + text_texture_init(&cache->no_media_hint, renderer, fonts->small, "Add MP4 or MKV files to ./media and relaunch.", COLOR_PALE_BLUE) != 0 || + text_texture_init(&cache->timeline_day, renderer, fonts->medium, "FRI 6/30", COLOR_TEXT_DARK) != 0 || + text_texture_init(&cache->footer_a, renderer, fonts->medium, "A", COLOR_TEXT_DARK) != 0 || + text_texture_init(&cache->footer_time, renderer, fonts->medium, "Time", COLOR_TEXT_DARK) != 0 || + text_texture_init(&cache->footer_b, renderer, fonts->medium, "B", COLOR_TEXT_LIGHT) != 0 || + text_texture_init(&cache->footer_theme, renderer, fonts->medium, "Theme", COLOR_TEXT_DARK) != 0 || + text_texture_init(&cache->footer_c, renderer, fonts->medium, "C", COLOR_TEXT_LIGHT) != 0 || + text_texture_init(&cache->footer_title, renderer, fonts->medium, "Title", COLOR_TEXT_DARK) != 0 || + text_texture_init(&cache->preview_badge, renderer, fonts->medium, "Nick * 56", COLOR_TEXT_LIGHT) != 0) { + ui_cache_destroy(cache); + return -1; + } + + if (channels && channels->count > 0) { + cache->channels = calloc((size_t) channels->count, sizeof(UiChannelCache)); + if (!cache->channels) { + ui_cache_destroy(cache); + return -1; + } + cache->channel_count = channels->count; + + for (int i = 0; i < channels->count; ++i) { + const Channel *channel = &channels->items[i]; + snprintf(buffer, sizeof(buffer), "%d", channel->number); + if (text_texture_init(&cache->channels[i].name_medium, renderer, fonts->medium, channel->name, COLOR_TEXT_LIGHT) != 0 || + text_texture_init(&cache->channels[i].number_medium, renderer, fonts->medium, buffer, COLOR_TEXT_LIGHT) != 0 || + text_texture_init(&cache->channels[i].file_small, renderer, fonts->small, channel->file_name, COLOR_PALE_BLUE) != 0 || + text_texture_init(&cache->channels[i].program_small_light, renderer, fonts->small, channel->program_title, COLOR_TEXT_LIGHT) != 0 || + text_texture_init(&cache->channels[i].program_medium_dark, renderer, fonts->medium, channel->program_title, COLOR_TEXT_DARK) != 0) { + ui_cache_destroy(cache); + return -1; + } + + snprintf(buffer, sizeof(buffer), "%.0f min - %s", channel->duration_seconds / 60.0, channel->file_name); + if (text_texture_init(&cache->channels[i].detail_small_dark, renderer, fonts->small, buffer, COLOR_TEXT_DARK) != 0) { + ui_cache_destroy(cache); + return -1; + } + } + } + + return 0; +} + +void ui_cache_destroy(UiCache *cache) { + if (!cache) { + return; + } + + text_texture_destroy(&cache->no_media_title); + text_texture_destroy(&cache->no_media_body); + text_texture_destroy(&cache->no_media_hint); + text_texture_destroy(&cache->timeline_day); + text_texture_destroy(&cache->footer_a); + text_texture_destroy(&cache->footer_time); + text_texture_destroy(&cache->footer_b); + text_texture_destroy(&cache->footer_theme); + text_texture_destroy(&cache->footer_c); + text_texture_destroy(&cache->footer_title); + text_texture_destroy(&cache->preview_badge); + + if (cache->timeline_labels) { + for (int i = 0; i < 4; ++i) { + text_texture_destroy(&cache->timeline_labels[i]); + } + free(cache->timeline_labels); + } + + if (cache->channels) { + for (int i = 0; i < cache->channel_count; ++i) { + text_texture_destroy(&cache->channels[i].name_medium); + text_texture_destroy(&cache->channels[i].number_medium); + text_texture_destroy(&cache->channels[i].file_small); + text_texture_destroy(&cache->channels[i].program_small_light); + text_texture_destroy(&cache->channels[i].program_medium_dark); + text_texture_destroy(&cache->channels[i].detail_small_dark); + } + free(cache->channels); + } + + memset(cache, 0, sizeof(*cache)); } int ui_load_fonts(UiFonts *fonts) { diff --git a/src/ui.h b/src/ui.h index c251352..523b4fa 100644 --- a/src/ui.h +++ b/src/ui.h @@ -14,18 +14,56 @@ typedef struct UiFonts { TTF_Font *large; } UiFonts; +typedef struct UiTextTexture { + SDL_Texture *texture; + int width; + int height; +} UiTextTexture; + +typedef struct UiChannelCache { + UiTextTexture name_medium; + UiTextTexture number_medium; + UiTextTexture file_small; + UiTextTexture program_small_light; + UiTextTexture program_medium_dark; + UiTextTexture detail_small_dark; +} UiChannelCache; + +typedef struct UiCache { + SDL_Renderer *renderer; + UiTextTexture no_media_title; + UiTextTexture no_media_body; + UiTextTexture no_media_hint; + UiTextTexture timeline_day; + UiTextTexture footer_a; + UiTextTexture footer_time; + UiTextTexture footer_b; + UiTextTexture footer_theme; + UiTextTexture footer_c; + UiTextTexture footer_title; + UiTextTexture preview_badge; + UiTextTexture *timeline_labels; + time_t timeline_label_slot; + UiChannelCache *channels; + int channel_count; +} UiCache; + void ui_render_fullscreen(SDL_Renderer *renderer, SDL_Texture *video_texture, int texture_width, int texture_height); void ui_render_guide(SDL_Renderer *renderer, SDL_Texture *video_texture, int texture_width, int texture_height, const UiFonts *fonts, + UiCache *cache, const ChannelList *channels, int active_channel, - time_t app_start_time, - time_t now); -void ui_render_no_media(SDL_Renderer *renderer, const UiFonts *fonts); + Uint64 app_start_ticks, + Uint64 now_ticks, + time_t now_wall); +void ui_render_no_media(SDL_Renderer *renderer, const UiCache *cache); int ui_load_fonts(UiFonts *fonts); void ui_destroy_fonts(UiFonts *fonts); +int ui_cache_init(UiCache *cache, SDL_Renderer *renderer, const UiFonts *fonts, const ChannelList *channels); +void ui_cache_destroy(UiCache *cache); #endif diff --git a/src/ui.o b/src/ui.o index de0ec25..8df4c7d 100644 Binary files a/src/ui.o and b/src/ui.o differ