diff --git a/src/app.c b/src/app.c index bf0127f..2a7aff4 100644 --- a/src/app.c +++ b/src/app.c @@ -331,7 +331,7 @@ void app_run(App *app) { in_blackout = player_is_in_blackout(&app->player); 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) { app->startup_handoff_active = 1; app->startup_handoff_until = SDL_GetTicks() + 400; diff --git a/src/app.o b/src/app.o index be65668..b3f2dc4 100644 Binary files a/src/app.o and b/src/app.o differ diff --git a/src/channel.c b/src/channel.c index 53e7f7b..38abc96 100644 --- a/src/channel.c +++ b/src/channel.c @@ -322,6 +322,17 @@ void channel_list_destroy(ChannelList *list) { 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 elapsed; diff --git a/src/channel.h b/src/channel.h index 4ab0dba..a85cbb9 100644 --- a/src/channel.h +++ b/src/channel.h @@ -29,6 +29,7 @@ typedef struct ChannelList { int channel_list_load(ChannelList *list, const char *media_dir); 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_precise(const Channel *channel, Uint64 app_start_ticks, Uint64 now_ticks); const ProgramEntry *channel_resolve_program(const Channel *channel, diff --git a/src/channel.o b/src/channel.o index 88915cf..e81015e 100644 Binary files a/src/channel.o and b/src/channel.o differ diff --git a/src/frame_queue.o b/src/frame_queue.o index 3eb478d..8459264 100644 Binary files a/src/frame_queue.o and b/src/frame_queue.o differ diff --git a/src/player.c b/src/player.c index 190d5e1..e30f03c 100644 --- a/src/player.c +++ b/src/player.c @@ -419,6 +419,80 @@ static int decode_context_open(DecodeContext *ctx, const ProgramEntry *program, 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) { DecoderThreadArgs *args = (DecoderThreadArgs *) userdata; Player *player = args->player; @@ -438,29 +512,42 @@ static int decode_thread_main(void *userdata) { ctx.audio_stream_index = -1; free(args); - program = channel_resolve_program(channel, player->app_start_ticks, SDL_GetTicks64(), &seek_seconds, ¤t_program_index); - if (!program || decode_context_open(&ctx, program, player) != 0) { + if (decode_context_sync_to_schedule(player, + channel, + &ctx, + &program, + ¤t_program_index, + &seek_seconds, + 1, + &queued_any_audio, + &queued_first_video) != 0) { decode_context_reset(&ctx); 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)) { + if (decode_context_sync_to_schedule(player, + channel, + &ctx, + &program, + ¤t_program_index, + &seek_seconds, + 0, + &queued_any_audio, + &queued_first_video) != 0) { + goto cleanup; + } + if (av_read_frame(ctx.format_context, ctx.packet) < 0) { - current_program_index = (current_program_index + 1) % channel->program_count; - program = channel_program_at_index(channel, current_program_index); - queued_any_audio = 0; - queued_first_video = 0; - if (!program || decode_context_open(&ctx, program, player) != 0) { + if (decode_context_sync_to_schedule(player, + channel, + &ctx, + &program, + ¤t_program_index, + &seek_seconds, + 1, + &queued_any_audio, + &queued_first_video) != 0) { goto cleanup; } continue; diff --git a/src/player.o b/src/player.o index 54c9795..2cb28d1 100644 Binary files a/src/player.o and b/src/player.o differ diff --git a/src/ui.c b/src/ui.c index 6fe3e6d..9e772c3 100644 --- a/src/ui.c +++ b/src/ui.c @@ -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); } -static void draw_timeline_header_cached(SDL_Renderer *renderer, const UiCache *cache, const GuideTheme *theme, SDL_Rect rect) { - int segments = 4; +static int timeline_marker_x(int guide_x_start, double pixels_per_minute, int minute_offset) { + 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_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->panel_border); - for (int i = 0; i < segments; ++i) { - int x = rect.x + (rect.w * i) / segments; + (void) timeline_width; + 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 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 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_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); @@ -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); } -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, grid_rect, theme->background_top, @@ -501,7 +530,7 @@ static void draw_grid_background(SDL_Renderer *renderer, const GuideTheme *theme theme->background_bottom); 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); set_draw_color(renderer, strong_line); 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; const Channel *selected_channel = NULL; 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) { 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) { 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)); + format_clock_label(label, sizeof(label), guide_view_start_time, 30 * i); text_texture_destroy(&cache->timeline_labels[i]); text_texture_init(&cache->timeline_labels[i], renderer, fonts->small, label, COLOR_TEXT_LIGHT); } cache->timeline_label_slot = now_wall / 60; cache->timeline_theme = theme; } - draw_timeline_header_cached(renderer, cache, theme, header_row); - draw_grid_background(renderer, theme, &grid, row_height, pixels_per_minute); + draw_timeline_header_cached(renderer, + 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) { start_index = 0; @@ -741,11 +778,10 @@ void ui_render_guide(SDL_Renderer *renderer, { const Channel *channel = &channels->items[channel_index]; - double live_position = 0.0; double program_seek = 0.0; 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); - time_t program_start_time = now_wall - (time_t) live_position; + double guide_view_start_seconds = (double) guide_view_start_time; + double program_start_time = (double) now_wall; int block_x = guide_x_start; int block_w = 48; 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) { continue; } - live_position = channel_live_position_precise(channel, app_start_ticks, now_ticks); - program_start_time = now_wall - (time_t) program_seek; - block_x = guide_x_start + (int) (((double) (program_start_time - guide_view_start_time)) / 60.0 * pixels_per_minute); + program_start_time -= program_seek; + block_x = guide_x_start + (int) ((((program_start_time - guide_view_start_seconds) / 60.0) * pixels_per_minute) + 0.5); 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}; title_rect = (SDL_Rect){block.x + 8, block.y + 8, block.w - 16, block.h - 16}; diff --git a/src/ui.o b/src/ui.o index c4a1f0e..470287a 100644 Binary files a/src/ui.o and b/src/ui.o differ