Add hw acceleration, improve video performance
This commit is contained in:
parent
0a250b05f3
commit
7c5d82e124
13 changed files with 273 additions and 72 deletions
30
src/app.c
30
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);
|
||||
|
|
|
|||
|
|
@ -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
BIN
src/app.o
Binary file not shown.
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
BIN
src/channel.o
BIN
src/channel.o
Binary file not shown.
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#define FRAME_QUEUE_CAPACITY 8
|
||||
#define FRAME_QUEUE_CAPACITY 12
|
||||
|
||||
typedef struct FrameData {
|
||||
unsigned char *buffer;
|
||||
|
|
|
|||
22
src/player.c
22
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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
BIN
src/player.o
BIN
src/player.o
Binary file not shown.
220
src/ui.c
220
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) {
|
||||
|
|
|
|||
44
src/ui.h
44
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
|
||||
|
|
|
|||
BIN
src/ui.o
BIN
src/ui.o
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue