Add hw acceleration, improve video performance

This commit is contained in:
markmental 2026-03-27 23:19:42 -04:00
commit 7c5d82e124
13 changed files with 273 additions and 72 deletions

View file

@ -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);

View file

@ -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);

BIN
src/app.o

Binary file not shown.

View file

@ -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);
}

View file

@ -1,6 +1,7 @@
#ifndef CHANNEL_H
#define CHANNEL_H
#include <SDL2/SDL.h>
#include <limits.h>
#include <time.h>
@ -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

Binary file not shown.

View file

@ -3,7 +3,7 @@
#include <SDL2/SDL.h>
#define FRAME_QUEUE_CAPACITY 8
#define FRAME_QUEUE_CAPACITY 12
typedef struct FrameData {
unsigned char *buffer;

View file

@ -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;

View file

@ -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);

Binary file not shown.

220
src/ui.c
View file

@ -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) {

View file

@ -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

BIN
src/ui.o

Binary file not shown.