Fix guide misalignment

This commit is contained in:
markmental 2026-03-28 10:56:27 -04:00
commit 8890d7f527
10 changed files with 167 additions and 33 deletions

View file

@ -331,7 +331,7 @@ void app_run(App *app) {
in_blackout = player_is_in_blackout(&app->player); in_blackout = player_is_in_blackout(&app->player);
now_ticks = SDL_GetTicks64(); now_ticks = SDL_GetTicks64();
now_wall = time(NULL); now_wall = channel_wall_time_from_ticks(app->app_start_time, app->app_start_ticks, now_ticks);
if (app->last_blackout_state && !in_blackout) { if (app->last_blackout_state && !in_blackout) {
app->startup_handoff_active = 1; app->startup_handoff_active = 1;
app->startup_handoff_until = SDL_GetTicks() + 400; app->startup_handoff_until = SDL_GetTicks() + 400;

BIN
src/app.o

Binary file not shown.

View file

@ -322,6 +322,17 @@ void channel_list_destroy(ChannelList *list) {
list->count = 0; list->count = 0;
} }
time_t channel_wall_time_from_ticks(time_t app_start_time, Uint64 app_start_ticks, Uint64 now_ticks) {
Uint64 elapsed_ticks;
if (now_ticks < app_start_ticks) {
now_ticks = app_start_ticks;
}
elapsed_ticks = now_ticks - app_start_ticks;
return app_start_time + (time_t) (elapsed_ticks / 1000);
}
double channel_live_position(const Channel *channel, time_t app_start_time, time_t now) { double channel_live_position(const Channel *channel, time_t app_start_time, time_t now) {
double elapsed; double elapsed;

View file

@ -29,6 +29,7 @@ typedef struct ChannelList {
int channel_list_load(ChannelList *list, const char *media_dir); int channel_list_load(ChannelList *list, const char *media_dir);
void channel_list_destroy(ChannelList *list); void channel_list_destroy(ChannelList *list);
time_t channel_wall_time_from_ticks(time_t app_start_time, Uint64 app_start_ticks, Uint64 now_ticks);
double channel_live_position(const Channel *channel, time_t app_start_time, time_t now); 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); double channel_live_position_precise(const Channel *channel, Uint64 app_start_ticks, Uint64 now_ticks);
const ProgramEntry *channel_resolve_program(const Channel *channel, const ProgramEntry *channel_resolve_program(const Channel *channel,

Binary file not shown.

Binary file not shown.

View file

@ -419,6 +419,80 @@ static int decode_context_open(DecodeContext *ctx, const ProgramEntry *program,
return 0; return 0;
} }
static int decode_context_seek_to_seconds(DecodeContext *ctx, double seek_seconds) {
int64_t seek_target;
if (!ctx || seek_seconds <= 0.0 || !ctx->format_context || ctx->video_stream_index < 0) {
return 0;
}
seek_target = (int64_t) (seek_seconds / av_q2d(ctx->video_time_base));
if (avformat_seek_file(ctx->format_context, ctx->video_stream_index, INT64_MIN, seek_target, INT64_MAX, 0) < 0) {
return -1;
}
avcodec_flush_buffers(ctx->video_codec_context);
if (ctx->audio_codec_context) {
avcodec_flush_buffers(ctx->audio_codec_context);
}
return 0;
}
static int decode_context_sync_to_schedule(Player *player,
const Channel *channel,
DecodeContext *ctx,
const ProgramEntry **program,
int *current_program_index,
double *seek_seconds,
int force_reopen,
int *queued_any_audio,
int *queued_first_video) {
const ProgramEntry *scheduled_program;
int scheduled_program_index = -1;
double scheduled_seek_seconds = 0.0;
if (!player || !channel || !ctx || !program || !current_program_index || !seek_seconds) {
return -1;
}
scheduled_program = channel_resolve_program(channel,
player->app_start_ticks,
SDL_GetTicks64(),
&scheduled_seek_seconds,
&scheduled_program_index);
if (!scheduled_program) {
return -1;
}
if (!force_reopen && *program == scheduled_program && *current_program_index == scheduled_program_index) {
*seek_seconds = scheduled_seek_seconds;
return 0;
}
if (decode_context_open(ctx, scheduled_program, player) != 0) {
return -1;
}
if (decode_context_seek_to_seconds(ctx, scheduled_seek_seconds) != 0) {
player_set_error(player, "Unable to seek to scheduled program position");
return -1;
}
frame_queue_clear(&player->frame_queue);
player_clear_audio(player);
*program = scheduled_program;
*current_program_index = scheduled_program_index;
*seek_seconds = scheduled_seek_seconds;
if (queued_any_audio) {
*queued_any_audio = 0;
}
if (queued_first_video) {
*queued_first_video = 0;
}
return 0;
}
static int decode_thread_main(void *userdata) { static int decode_thread_main(void *userdata) {
DecoderThreadArgs *args = (DecoderThreadArgs *) userdata; DecoderThreadArgs *args = (DecoderThreadArgs *) userdata;
Player *player = args->player; Player *player = args->player;
@ -438,29 +512,42 @@ static int decode_thread_main(void *userdata) {
ctx.audio_stream_index = -1; ctx.audio_stream_index = -1;
free(args); free(args);
program = channel_resolve_program(channel, player->app_start_ticks, SDL_GetTicks64(), &seek_seconds, &current_program_index); if (decode_context_sync_to_schedule(player,
if (!program || decode_context_open(&ctx, program, player) != 0) { channel,
&ctx,
&program,
&current_program_index,
&seek_seconds,
1,
&queued_any_audio,
&queued_first_video) != 0) {
decode_context_reset(&ctx); decode_context_reset(&ctx);
return -1; return -1;
} }
if (seek_seconds > 0.0) {
int64_t seek_target = (int64_t) (seek_seconds / av_q2d(ctx.video_time_base));
avformat_seek_file(ctx.format_context, ctx.video_stream_index, INT64_MIN, seek_target, INT64_MAX, 0);
avcodec_flush_buffers(ctx.video_codec_context);
if (ctx.audio_codec_context) {
avcodec_flush_buffers(ctx.audio_codec_context);
}
}
player_clear_audio(player);
while (!should_stop(player)) { while (!should_stop(player)) {
if (decode_context_sync_to_schedule(player,
channel,
&ctx,
&program,
&current_program_index,
&seek_seconds,
0,
&queued_any_audio,
&queued_first_video) != 0) {
goto cleanup;
}
if (av_read_frame(ctx.format_context, ctx.packet) < 0) { if (av_read_frame(ctx.format_context, ctx.packet) < 0) {
current_program_index = (current_program_index + 1) % channel->program_count; if (decode_context_sync_to_schedule(player,
program = channel_program_at_index(channel, current_program_index); channel,
queued_any_audio = 0; &ctx,
queued_first_video = 0; &program,
if (!program || decode_context_open(&ctx, program, player) != 0) { &current_program_index,
&seek_seconds,
1,
&queued_any_audio,
&queued_first_video) != 0) {
goto cleanup; goto cleanup;
} }
continue; continue;

Binary file not shown.

View file

@ -378,8 +378,18 @@ static void format_clock_label(char *buffer, size_t buffer_size, time_t now, int
strftime(buffer, buffer_size, "%I:%M %p", &local_time); strftime(buffer, buffer_size, "%I:%M %p", &local_time);
} }
static void draw_timeline_header_cached(SDL_Renderer *renderer, const UiCache *cache, const GuideTheme *theme, SDL_Rect rect) { static int timeline_marker_x(int guide_x_start, double pixels_per_minute, int minute_offset) {
int segments = 4; return guide_x_start + (int) ((minute_offset * pixels_per_minute) + 0.5);
}
static void draw_timeline_header_cached(SDL_Renderer *renderer,
const UiCache *cache,
const GuideTheme *theme,
SDL_Rect rect,
int guide_x_start,
double pixels_per_minute,
int timeline_width,
int visible_minutes) {
SDL_Color ribbon_text = ensure_contrast(theme->ribbon_text, theme->ribbon_mid); SDL_Color ribbon_text = ensure_contrast(theme->ribbon_text, theme->ribbon_mid);
SDL_Color ribbon_shadow = color_luma(theme->ribbon_mid) < 120 ? color_with_alpha(COLOR_BLACK, 255) : color_with_alpha(COLOR_TEXT_LIGHT, 255); SDL_Color ribbon_shadow = color_luma(theme->ribbon_mid) < 120 ? color_with_alpha(COLOR_BLACK, 255) : color_with_alpha(COLOR_TEXT_LIGHT, 255);
@ -391,11 +401,25 @@ static void draw_timeline_header_cached(SDL_Renderer *renderer, const UiCache *c
theme->gloss, theme->gloss,
theme->panel_border); theme->panel_border);
for (int i = 0; i < segments; ++i) { (void) timeline_width;
int x = rect.x + (rect.w * i) / segments; for (int i = 0; i < 4; ++i) {
int minute_offset = (visible_minutes / 3) * i;
int x = timeline_marker_x(guide_x_start, pixels_per_minute, minute_offset);
int centered_x = x - cache->timeline_labels[i].width / 2; int centered_x = x - cache->timeline_labels[i].width / 2;
int min_center_x = guide_x_start;
int max_center_x = guide_x_start + timeline_width - cache->timeline_labels[i].width;
SDL_Rect shadow_dst = {centered_x + 1, rect.y + 11, cache->timeline_labels[i].width, cache->timeline_labels[i].height}; SDL_Rect shadow_dst = {centered_x + 1, rect.y + 11, cache->timeline_labels[i].width, cache->timeline_labels[i].height};
SDL_Rect text_dst = {centered_x, rect.y + 10, cache->timeline_labels[i].width, cache->timeline_labels[i].height}; SDL_Rect text_dst = {centered_x, rect.y + 10, cache->timeline_labels[i].width, cache->timeline_labels[i].height};
if (centered_x < min_center_x) {
centered_x = min_center_x;
}
if (centered_x > max_center_x) {
centered_x = max_center_x;
}
shadow_dst.x = centered_x + 1;
text_dst.x = centered_x;
SDL_SetTextureColorMod(cache->timeline_labels[i].texture, ribbon_shadow.r, ribbon_shadow.g, ribbon_shadow.b); SDL_SetTextureColorMod(cache->timeline_labels[i].texture, ribbon_shadow.r, ribbon_shadow.g, ribbon_shadow.b);
SDL_RenderCopy(renderer, cache->timeline_labels[i].texture, NULL, &shadow_dst); SDL_RenderCopy(renderer, cache->timeline_labels[i].texture, NULL, &shadow_dst);
SDL_SetTextureColorMod(cache->timeline_labels[i].texture, ribbon_text.r, ribbon_text.g, ribbon_text.b); SDL_SetTextureColorMod(cache->timeline_labels[i].texture, ribbon_text.r, ribbon_text.g, ribbon_text.b);
@ -493,7 +517,12 @@ static void draw_info_panel(SDL_Renderer *renderer,
draw_text_clipped(renderer, fonts->small, description, &clip_rect, rect->x + 18, rect->y + 148, panel_text); draw_text_clipped(renderer, fonts->small, description, &clip_rect, rect->x + 18, rect->y + 148, panel_text);
} }
static void draw_grid_background(SDL_Renderer *renderer, const GuideTheme *theme, const SDL_Rect *grid_rect, int row_height, double pixels_per_minute) { static void draw_grid_background(SDL_Renderer *renderer,
const GuideTheme *theme,
const SDL_Rect *grid_rect,
int guide_x_start,
int row_height,
double pixels_per_minute) {
fill_three_stop_gradient(renderer, fill_three_stop_gradient(renderer,
grid_rect, grid_rect,
theme->background_top, theme->background_top,
@ -501,7 +530,7 @@ static void draw_grid_background(SDL_Renderer *renderer, const GuideTheme *theme
theme->background_bottom); theme->background_bottom);
for (int minute = 0; minute <= 90; minute += 30) { for (int minute = 0; minute <= 90; minute += 30) {
int x = GUIDE_X_START + (int) (minute * pixels_per_minute); int x = timeline_marker_x(guide_x_start, pixels_per_minute, minute);
SDL_Color strong_line = ensure_contrast(theme->grid_line, theme->background_mid); SDL_Color strong_line = ensure_contrast(theme->grid_line, theme->background_mid);
set_draw_color(renderer, strong_line); set_draw_color(renderer, strong_line);
SDL_RenderDrawLine(renderer, x, grid_rect->y, x, grid_rect->y + grid_rect->h); SDL_RenderDrawLine(renderer, x, grid_rect->y, x, grid_rect->y + grid_rect->h);
@ -674,6 +703,7 @@ void ui_render_guide(SDL_Renderer *renderer,
int start_index = active_channel - 2; int start_index = active_channel - 2;
const Channel *selected_channel = NULL; const Channel *selected_channel = NULL;
double pixels_per_minute = timeline_w / 90.0; double pixels_per_minute = timeline_w / 90.0;
time_t guide_view_start_time = now_wall - (30 * 60);
if (channels && channels->count > 0 && active_channel >= 0 && active_channel < channels->count) { if (channels && channels->count > 0 && active_channel >= 0 && active_channel < channels->count) {
selected_channel = &channels->items[active_channel]; selected_channel = &channels->items[active_channel];
@ -693,15 +723,22 @@ void ui_render_guide(SDL_Renderer *renderer,
if (cache->timeline_label_slot != now_wall / 60 || cache->timeline_theme != theme) { if (cache->timeline_label_slot != now_wall / 60 || cache->timeline_theme != theme) {
char label[32]; char label[32];
for (int i = 0; i < 4; ++i) { for (int i = 0; i < 4; ++i) {
format_clock_label(label, sizeof(label), now_wall, (int) ((TIMELINE_VISIBLE_SECONDS / 60.0 / 4) * i)); format_clock_label(label, sizeof(label), guide_view_start_time, 30 * i);
text_texture_destroy(&cache->timeline_labels[i]); text_texture_destroy(&cache->timeline_labels[i]);
text_texture_init(&cache->timeline_labels[i], renderer, fonts->small, label, COLOR_TEXT_LIGHT); text_texture_init(&cache->timeline_labels[i], renderer, fonts->small, label, COLOR_TEXT_LIGHT);
} }
cache->timeline_label_slot = now_wall / 60; cache->timeline_label_slot = now_wall / 60;
cache->timeline_theme = theme; cache->timeline_theme = theme;
} }
draw_timeline_header_cached(renderer, cache, theme, header_row); draw_timeline_header_cached(renderer,
draw_grid_background(renderer, theme, &grid, row_height, pixels_per_minute); cache,
theme,
header_row,
guide_x_start,
pixels_per_minute,
timeline_w,
(int) (TIMELINE_VISIBLE_SECONDS / 60.0));
draw_grid_background(renderer, theme, &grid, guide_x_start, row_height, pixels_per_minute);
if (start_index < 0) { if (start_index < 0) {
start_index = 0; start_index = 0;
@ -741,11 +778,10 @@ void ui_render_guide(SDL_Renderer *renderer,
{ {
const Channel *channel = &channels->items[channel_index]; const Channel *channel = &channels->items[channel_index];
double live_position = 0.0;
double program_seek = 0.0; double program_seek = 0.0;
const ProgramEntry *program = channel_resolve_program(channel, app_start_ticks, now_ticks, &program_seek, NULL); const ProgramEntry *program = channel_resolve_program(channel, app_start_ticks, now_ticks, &program_seek, NULL);
time_t guide_view_start_time = now_wall - (30 * 60); double guide_view_start_seconds = (double) guide_view_start_time;
time_t program_start_time = now_wall - (time_t) live_position; double program_start_time = (double) now_wall;
int block_x = guide_x_start; int block_x = guide_x_start;
int block_w = 48; int block_w = 48;
SDL_Rect block = {guide_x_start, timeline_rect.y + 4, 48, timeline_rect.h - 8}; SDL_Rect block = {guide_x_start, timeline_rect.y + 4, 48, timeline_rect.h - 8};
@ -764,9 +800,8 @@ void ui_render_guide(SDL_Renderer *renderer,
if (!program) { if (!program) {
continue; continue;
} }
live_position = channel_live_position_precise(channel, app_start_ticks, now_ticks); program_start_time -= program_seek;
program_start_time = now_wall - (time_t) program_seek; block_x = guide_x_start + (int) ((((program_start_time - guide_view_start_seconds) / 60.0) * pixels_per_minute) + 0.5);
block_x = guide_x_start + (int) (((double) (program_start_time - guide_view_start_time)) / 60.0 * pixels_per_minute);
block_w = (int) ((program->duration_seconds / 60.0) * pixels_per_minute); block_w = (int) ((program->duration_seconds / 60.0) * pixels_per_minute);
block = (SDL_Rect){block_x, timeline_rect.y + 4, SDL_max(block_w, 48), timeline_rect.h - 8}; block = (SDL_Rect){block_x, timeline_rect.y + 4, SDL_max(block_w, 48), timeline_rect.h - 8};
title_rect = (SDL_Rect){block.x + 8, block.y + 8, block.w - 16, block.h - 16}; title_rect = (SDL_Rect){block.x + 8, block.y + 8, block.w - 16, block.h - 16};

BIN
src/ui.o

Binary file not shown.