From b84a3060c105389100064853cfd49ee92533db44 Mon Sep 17 00:00:00 2001 From: markmental Date: Sat, 28 Mar 2026 00:25:06 -0400 Subject: [PATCH] New directory based system for channels --- README.md | 25 +++- src/channel.c | 256 +++++++++++++++++++++++++++++++----- src/channel.h | 16 ++- src/channel.o | Bin 6016 -> 9288 bytes src/player.c | 357 +++++++++++++++++++++++++------------------------- src/player.o | Bin 21808 -> 21296 bytes src/ui.c | 53 +++++--- src/ui.o | Bin 24768 -> 25328 bytes 8 files changed, 466 insertions(+), 241 deletions(-) diff --git a/README.md b/README.md index 8051d5e..76eb621 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,29 @@ make ## Media -The player scans `./media` for supported video files and converts filenames into channel names and current program titles. +The player scans `./media` for channel directories. Each subdirectory is treated as one channel, and the supported video files inside it are played in alphabetical order as that channel's schedule. -Current sample channels: +Optional metadata can be provided with `description.txt` inside each channel directory: -- `media/reading.mp4` -- `media/computers.mp4` +```txt +name=Reading +number=56 +description=Books, documentaries, and reading-focused programming. +``` + +Example layout: + +```txt +media/ + reading/ + description.txt + 01-intro.mp4 + 02-feature.mp4 + computers/ + description.txt + 01-chronicles.mp4 + 02-demo.mp4 +``` ## Fonts diff --git a/src/channel.c b/src/channel.c index 10a9916..53e7f7b 100644 --- a/src/channel.c +++ b/src/channel.c @@ -8,6 +8,7 @@ #include #include #include +#include static int has_supported_extension(const char *name) { const char *dot = strrchr(name, '.'); @@ -23,6 +24,11 @@ static int has_supported_extension(const char *name) { strcasecmp(dot, "m4v") == 0; } +static int path_is_directory(const char *path) { + struct stat st; + return path && stat(path, &st) == 0 && S_ISDIR(st.st_mode); +} + static void trim_extension(char *text) { char *dot = strrchr(text, '.'); if (dot) { @@ -38,42 +44,45 @@ static void make_title_case(char *text) { new_word = 1; continue; } - if (isspace((unsigned char) text[i])) { new_word = 1; continue; } - text[i] = new_word ? (char) toupper((unsigned char) text[i]) : (char) tolower((unsigned char) text[i]); new_word = 0; } } -static void derive_channel_name(const char *file_name, char *out, size_t out_size) { - char temp[128]; - snprintf(temp, sizeof(temp), "%s", file_name); +static void safe_copy(char *out, size_t out_size, const char *value) { + if (!out || out_size == 0) { + return; + } + snprintf(out, out_size, "%s", value ? value : ""); +} + +static void derive_display_name(const char *source, char *out, size_t out_size) { + char temp[256]; + safe_copy(temp, sizeof(temp), source); trim_extension(temp); - - if (strstr(temp, "reading") || strstr(temp, "reads")) { - snprintf(out, out_size, "Reading"); - return; - } - - if (strstr(temp, "computer") || strstr(temp, "half-life")) { - snprintf(out, out_size, "Computers"); - return; - } - make_title_case(temp); - snprintf(out, out_size, "%s", temp); + safe_copy(out, out_size, temp); } static void derive_program_title(const char *file_name, char *out, size_t out_size) { - char temp[128]; - snprintf(temp, sizeof(temp), "%s", file_name); - trim_extension(temp); - make_title_case(temp); - snprintf(out, out_size, "%s", temp); + derive_display_name(file_name, out, out_size); +} + +static char *trim_whitespace(char *text) { + char *end; + while (*text && isspace((unsigned char) *text)) { + text++; + } + end = text + strlen(text); + while (end > text && isspace((unsigned char) end[-1])) { + end--; + } + *end = '\0'; + return text; } static double probe_duration_seconds(const char *file_path) { @@ -92,10 +101,126 @@ static double probe_duration_seconds(const char *file_path) { return duration; } +static int compare_programs(const void *left, const void *right) { + const ProgramEntry *a = (const ProgramEntry *) left; + const ProgramEntry *b = (const ProgramEntry *) right; + return strcmp(a->file_name, b->file_name); +} + static int compare_channels(const void *left, const void *right) { const Channel *a = (const Channel *) left; const Channel *b = (const Channel *) right; - return strcmp(a->file_name, b->file_name); + int a_has_number = a->number > 0; + int b_has_number = b->number > 0; + + if (a_has_number && b_has_number && a->number != b->number) { + return a->number - b->number; + } + if (a_has_number != b_has_number) { + return a_has_number ? -1 : 1; + } + return strcmp(a->name, b->name); +} + +static int parse_channel_metadata(Channel *channel, const char *channel_dir) { + char metadata_path[PATH_MAX]; + FILE *file; + char line[512]; + + snprintf(metadata_path, sizeof(metadata_path), "%s/description.txt", channel_dir); + file = fopen(metadata_path, "r"); + if (!file) { + return 0; + } + + while (fgets(line, sizeof(line), file) != NULL) { + char *equals = strchr(line, '='); + char *key; + char *value; + if (!equals) { + continue; + } + *equals = '\0'; + key = trim_whitespace(line); + value = trim_whitespace(equals + 1); + if (strcasecmp(key, "name") == 0) { + safe_copy(channel->name, sizeof(channel->name), value); + } else if (strcasecmp(key, "number") == 0) { + channel->number = atoi(value); + } else if (strcasecmp(key, "description") == 0) { + safe_copy(channel->description, sizeof(channel->description), value); + } + } + + fclose(file); + return 0; +} + +static int load_channel_programs(Channel *channel, const char *channel_dir) { + DIR *directory; + struct dirent *entry; + int capacity = 8; + + directory = opendir(channel_dir); + if (!directory) { + return -1; + } + + channel->programs = calloc((size_t) capacity, sizeof(ProgramEntry)); + if (!channel->programs) { + closedir(directory); + return -1; + } + + while ((entry = readdir(directory)) != NULL) { + ProgramEntry *program; + char file_path[PATH_MAX]; + + if (entry->d_name[0] == '.' || !has_supported_extension(entry->d_name)) { + continue; + } + + snprintf(file_path, sizeof(file_path), "%s/%s", channel_dir, entry->d_name); + if (path_is_directory(file_path)) { + continue; + } + + if (channel->program_count == capacity) { + ProgramEntry *resized; + capacity *= 2; + resized = realloc(channel->programs, (size_t) capacity * sizeof(ProgramEntry)); + if (!resized) { + closedir(directory); + return -1; + } + channel->programs = resized; + } + + program = &channel->programs[channel->program_count]; + memset(program, 0, sizeof(*program)); + safe_copy(program->file_name, sizeof(program->file_name), entry->d_name); + safe_copy(program->file_path, sizeof(program->file_path), file_path); + derive_program_title(entry->d_name, program->program_title, sizeof(program->program_title)); + program->duration_seconds = probe_duration_seconds(program->file_path); + channel->program_count += 1; + } + + closedir(directory); + + if (channel->program_count == 0) { + free(channel->programs); + channel->programs = NULL; + return 0; + } + + qsort(channel->programs, (size_t) channel->program_count, sizeof(ProgramEntry), compare_programs); + channel->total_duration_seconds = 0.0; + for (int i = 0; i < channel->program_count; ++i) { + channel->programs[i].start_offset_seconds = channel->total_duration_seconds; + channel->total_duration_seconds += channel->programs[i].duration_seconds; + } + + return channel->program_count; } int channel_list_load(ChannelList *list, const char *media_dir) { @@ -122,8 +247,15 @@ int channel_list_load(ChannelList *list, const char *media_dir) { while ((entry = readdir(directory)) != NULL) { Channel *channel; + char channel_dir[PATH_MAX]; + int load_result; - if (entry->d_name[0] == '.' || !has_supported_extension(entry->d_name)) { + if (entry->d_name[0] == '.') { + continue; + } + + snprintf(channel_dir, sizeof(channel_dir), "%s/%s", media_dir, entry->d_name); + if (!path_is_directory(channel_dir)) { continue; } @@ -141,12 +273,20 @@ int channel_list_load(ChannelList *list, const char *media_dir) { channel = &list->items[list->count]; memset(channel, 0, sizeof(*channel)); - channel->number = 50 + list->count; - snprintf(channel->file_name, sizeof(channel->file_name), "%s", entry->d_name); - snprintf(channel->file_path, sizeof(channel->file_path), "%s/%s", media_dir, entry->d_name); - derive_channel_name(entry->d_name, channel->name, sizeof(channel->name)); - derive_program_title(entry->d_name, channel->program_title, sizeof(channel->program_title)); - channel->duration_seconds = probe_duration_seconds(channel->file_path); + derive_display_name(entry->d_name, channel->name, sizeof(channel->name)); + safe_copy(channel->description, sizeof(channel->description), "Local programming lineup."); + parse_channel_metadata(channel, channel_dir); + load_result = load_channel_programs(channel, channel_dir); + if (load_result < 0) { + closedir(directory); + channel_list_destroy(list); + return -1; + } + if (load_result == 0 || channel->total_duration_seconds <= 0.0) { + free(channel->programs); + memset(channel, 0, sizeof(*channel)); + continue; + } list->count += 1; } @@ -159,7 +299,9 @@ int channel_list_load(ChannelList *list, const char *media_dir) { qsort(list->items, (size_t) list->count, sizeof(Channel), compare_channels); for (int i = 0; i < list->count; ++i) { - list->items[i].number = 50 + i; + if (list->items[i].number <= 0) { + list->items[i].number = 50 + i; + } } return list->count; @@ -170,6 +312,11 @@ void channel_list_destroy(ChannelList *list) { return; } + if (list->items) { + for (int i = 0; i < list->count; ++i) { + free(list->items[i].programs); + } + } free(list->items); list->items = NULL; list->count = 0; @@ -178,7 +325,7 @@ void channel_list_destroy(ChannelList *list) { double channel_live_position(const Channel *channel, time_t app_start_time, time_t now) { double elapsed; - if (!channel || channel->duration_seconds <= 0.0) { + if (!channel || channel->total_duration_seconds <= 0.0) { return 0.0; } @@ -187,13 +334,13 @@ double channel_live_position(const Channel *channel, time_t app_start_time, time elapsed = 0.0; } - return fmod(elapsed, channel->duration_seconds); + return fmod(elapsed, channel->total_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) { + if (!channel || channel->total_duration_seconds <= 0.0) { return 0.0; } @@ -202,5 +349,44 @@ double channel_live_position_precise(const Channel *channel, Uint64 app_start_ti } elapsed = (double) (now_ticks - app_start_ticks) / 1000.0; - return fmod(elapsed, channel->duration_seconds); + return fmod(elapsed, channel->total_duration_seconds); +} + +const ProgramEntry *channel_program_at_index(const Channel *channel, int program_index) { + if (!channel || program_index < 0 || program_index >= channel->program_count) { + return NULL; + } + return &channel->programs[program_index]; +} + +const ProgramEntry *channel_resolve_program(const Channel *channel, + Uint64 app_start_ticks, + Uint64 now_ticks, + double *program_seek_seconds, + int *program_index) { + double channel_offset; + + if (!channel || channel->program_count == 0 || channel->total_duration_seconds <= 0.0) { + return NULL; + } + + channel_offset = channel_live_position_precise(channel, app_start_ticks, now_ticks); + for (int i = 0; i < channel->program_count; ++i) { + const ProgramEntry *program = &channel->programs[i]; + double end_offset = program->start_offset_seconds + program->duration_seconds; + if (channel_offset < end_offset || i == channel->program_count - 1) { + if (program_seek_seconds) { + *program_seek_seconds = channel_offset - program->start_offset_seconds; + if (*program_seek_seconds < 0.0) { + *program_seek_seconds = 0.0; + } + } + if (program_index) { + *program_index = i; + } + return program; + } + } + + return &channel->programs[0]; } diff --git a/src/channel.h b/src/channel.h index fcb6354..4ab0dba 100644 --- a/src/channel.h +++ b/src/channel.h @@ -8,11 +8,19 @@ typedef struct Channel { int number; char name[64]; + char description[256]; + double total_duration_seconds; + struct ProgramEntry *programs; + int program_count; +} Channel; + +typedef struct ProgramEntry { char file_path[PATH_MAX]; char file_name[128]; char program_title[128]; double duration_seconds; -} Channel; + double start_offset_seconds; +} ProgramEntry; typedef struct ChannelList { Channel *items; @@ -23,5 +31,11 @@ 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); +const ProgramEntry *channel_resolve_program(const Channel *channel, + Uint64 app_start_ticks, + Uint64 now_ticks, + double *program_seek_seconds, + int *program_index); +const ProgramEntry *channel_program_at_index(const Channel *channel, int program_index); #endif diff --git a/src/channel.o b/src/channel.o index 619064d8d3d4ad3c9879a26035bccbbc447ea6e6..88915cfb79676389a326c8a1222e183101295c87 100644 GIT binary patch literal 9288 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 WDyBofz%MG#ytY(M7e>2M{(k@mwbUd4 literal 6016 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 diff --git a/src/player.c b/src/player.c index a57f849..190d5e1 100644 --- a/src/player.c +++ b/src/player.c @@ -184,6 +184,7 @@ static int queue_audio_frame(Player *player, AVCodecContext *audio_codec_context, AVRational audio_time_base, AVFrame *audio_frame, + double pts_base_offset, int *queued_any_audio) { uint8_t **converted_data = NULL; int out_rate; @@ -269,7 +270,7 @@ static int queue_audio_frame(Player *player, audio_pts = audio_frame->best_effort_timestamp == AV_NOPTS_VALUE ? NAN - : audio_frame->best_effort_timestamp * av_q2d(audio_time_base); + : pts_base_offset + (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.18); @@ -303,187 +304,211 @@ static int queue_audio_frame(Player *player, return 0; } +typedef struct DecodeContext { + AVFormatContext *format_context; + AVCodecContext *video_codec_context; + AVCodecContext *audio_codec_context; + struct SwsContext *sws_context; + SwrContext *swr_context; + AVPacket *packet; + AVFrame *decoded_frame; + AVFrame *audio_frame; + AVRational video_time_base; + AVRational audio_time_base; + int video_stream_index; + int audio_stream_index; +} DecodeContext; + +static void decode_context_reset(DecodeContext *ctx) { + if (!ctx) { + return; + } + if (ctx->packet) av_packet_free(&ctx->packet); + if (ctx->decoded_frame) av_frame_free(&ctx->decoded_frame); + if (ctx->audio_frame) av_frame_free(&ctx->audio_frame); + if (ctx->video_codec_context) avcodec_free_context(&ctx->video_codec_context); + if (ctx->audio_codec_context) avcodec_free_context(&ctx->audio_codec_context); + if (ctx->format_context) avformat_close_input(&ctx->format_context); + if (ctx->sws_context) sws_freeContext(ctx->sws_context); + if (ctx->swr_context) swr_free(&ctx->swr_context); + memset(ctx, 0, sizeof(*ctx)); + ctx->video_stream_index = -1; + ctx->audio_stream_index = -1; +} + +static int decode_context_open(DecodeContext *ctx, const ProgramEntry *program, Player *player) { + const AVCodec *video_codec = NULL; + const AVCodec *audio_codec = NULL; + + decode_context_reset(ctx); + ctx->video_stream_index = -1; + ctx->audio_stream_index = -1; + + if (avformat_open_input(&ctx->format_context, program->file_path, NULL, NULL) < 0) { + player_set_error(player, "Unable to open media file"); + return -1; + } + if (avformat_find_stream_info(ctx->format_context, NULL) < 0) { + player_set_error(player, "Unable to read stream metadata"); + return -1; + } + for (unsigned int i = 0; i < ctx->format_context->nb_streams; ++i) { + if (ctx->format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + ctx->video_stream_index = (int) i; + } else if (ctx->format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && ctx->audio_stream_index < 0) { + ctx->audio_stream_index = (int) i; + } + } + if (ctx->video_stream_index < 0) { + player_set_error(player, "No video stream found"); + return -1; + } + + video_codec = avcodec_find_decoder(ctx->format_context->streams[ctx->video_stream_index]->codecpar->codec_id); + if (!video_codec) { + player_set_error(player, "Unsupported video codec"); + return -1; + } + ctx->video_codec_context = avcodec_alloc_context3(video_codec); + if (!ctx->video_codec_context || + avcodec_parameters_to_context(ctx->video_codec_context, ctx->format_context->streams[ctx->video_stream_index]->codecpar) < 0 || + avcodec_open2(ctx->video_codec_context, video_codec, NULL) < 0) { + player_set_error(player, "Unable to initialize decoder"); + return -1; + } + + if (ctx->audio_stream_index >= 0) { + audio_codec = avcodec_find_decoder(ctx->format_context->streams[ctx->audio_stream_index]->codecpar->codec_id); + if (audio_codec) { + ctx->audio_codec_context = avcodec_alloc_context3(audio_codec); + if (!ctx->audio_codec_context || + avcodec_parameters_to_context(ctx->audio_codec_context, ctx->format_context->streams[ctx->audio_stream_index]->codecpar) < 0 || + avcodec_open2(ctx->audio_codec_context, audio_codec, NULL) < 0) { + player_set_error(player, "Unable to initialize audio decoder"); + return -1; + } + ctx->audio_time_base = ctx->format_context->streams[ctx->audio_stream_index]->time_base; + } + } + + ctx->packet = av_packet_alloc(); + ctx->decoded_frame = av_frame_alloc(); + ctx->audio_frame = av_frame_alloc(); + if (!ctx->packet || !ctx->decoded_frame || !ctx->audio_frame) { + player_set_error(player, "Unable to allocate FFmpeg frame buffers"); + return -1; + } + + ctx->video_time_base = ctx->format_context->streams[ctx->video_stream_index]->time_base; + ctx->sws_context = sws_getContext(ctx->video_codec_context->width, + ctx->video_codec_context->height, + ctx->video_codec_context->pix_fmt, + ctx->video_codec_context->width, + ctx->video_codec_context->height, + AV_PIX_FMT_YUV420P, + SWS_BILINEAR, + NULL, + NULL, + NULL); + if (!ctx->sws_context) { + player_set_error(player, "Unable to initialize scaler"); + return -1; + } + + player->has_audio_stream = ctx->audio_codec_context != NULL; + return 0; +} + static int decode_thread_main(void *userdata) { DecoderThreadArgs *args = (DecoderThreadArgs *) userdata; Player *player = args->player; const Channel *channel = args->channel; - AVFormatContext *format_context = NULL; - AVCodecContext *codec_context = NULL; - AVCodecContext *audio_codec_context = NULL; - AVPacket *packet = NULL; - AVFrame *decoded_frame = NULL; - AVFrame *audio_frame = NULL; - struct SwsContext *sws_context = NULL; - SwrContext *swr_context = NULL; - const AVCodec *codec = NULL; - const AVCodec *audio_codec = NULL; - int video_stream_index = -1; - int audio_stream_index = -1; - int rc = -1; + DecodeContext ctx; + const ProgramEntry *program; + int current_program_index = 0; double seek_seconds = 0.0; - AVRational time_base; - AVRational audio_time_base = {0}; int queued_any_audio = 0; int queued_first_video = 0; + int rc = -1; - #define MAYBE_MARK_PREROLL_READY() \ - do { \ - if (queued_first_video && (queued_any_audio || !player->has_audio_stream)) { \ - player_mark_preroll_ready(player); \ - } \ - } while (0) + #define MAYBE_MARK_PREROLL_READY() do { if (queued_first_video && (queued_any_audio || !player->has_audio_stream)) { player_mark_preroll_ready(player); } } while (0) + memset(&ctx, 0, sizeof(ctx)); + ctx.video_stream_index = -1; + ctx.audio_stream_index = -1; free(args); - if (avformat_open_input(&format_context, channel->file_path, NULL, NULL) < 0) { - player_set_error(player, "Unable to open media file"); + 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) { + decode_context_reset(&ctx); return -1; } - if (avformat_find_stream_info(format_context, NULL) < 0) { - player_set_error(player, "Unable to read stream metadata"); - goto cleanup; - } - - for (unsigned int i = 0; i < format_context->nb_streams; ++i) { - if (format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { - video_stream_index = (int) i; - } else if (format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && audio_stream_index < 0) { - audio_stream_index = (int) i; + 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); } } - - if (video_stream_index < 0) { - player_set_error(player, "No video stream found"); - goto cleanup; - } - - codec = avcodec_find_decoder(format_context->streams[video_stream_index]->codecpar->codec_id); - if (!codec) { - player_set_error(player, "Unsupported video codec"); - goto cleanup; - } - - codec_context = avcodec_alloc_context3(codec); - if (!codec_context) { - player_set_error(player, "Unable to allocate codec context"); - goto cleanup; - } - - if (avcodec_parameters_to_context(codec_context, format_context->streams[video_stream_index]->codecpar) < 0 || - avcodec_open2(codec_context, codec, NULL) < 0) { - player_set_error(player, "Unable to initialize decoder"); - goto cleanup; - } - - if (audio_stream_index >= 0) { - audio_codec = avcodec_find_decoder(format_context->streams[audio_stream_index]->codecpar->codec_id); - if (audio_codec) { - audio_codec_context = avcodec_alloc_context3(audio_codec); - if (!audio_codec_context) { - player_set_error(player, "Unable to allocate audio decoder context"); - goto cleanup; - } - - if (avcodec_parameters_to_context(audio_codec_context, format_context->streams[audio_stream_index]->codecpar) < 0 || - avcodec_open2(audio_codec_context, audio_codec, NULL) < 0) { - player_set_error(player, "Unable to initialize audio decoder"); - goto cleanup; - } - - audio_time_base = format_context->streams[audio_stream_index]->time_base; - } - } - - decoded_frame = av_frame_alloc(); - audio_frame = av_frame_alloc(); - packet = av_packet_alloc(); - if (!decoded_frame || !audio_frame || !packet) { - player_set_error(player, "Unable to allocate FFmpeg frame buffers"); - goto cleanup; - } - - time_base = format_context->streams[video_stream_index]->time_base; - player->has_audio_stream = audio_codec_context != NULL; - sws_context = sws_getContext(codec_context->width, - codec_context->height, - codec_context->pix_fmt, - codec_context->width, - codec_context->height, - AV_PIX_FMT_YUV420P, - SWS_BILINEAR, - NULL, - NULL, - NULL); - if (!sws_context) { - player_set_error(player, "Unable to initialize scaler"); - goto cleanup; - } - - 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); - avcodec_flush_buffers(codec_context); - if (audio_codec_context) { - avcodec_flush_buffers(audio_codec_context); - } - player_clear_audio(player); - } + player_clear_audio(player); while (!should_stop(player)) { - if (av_read_frame(format_context, packet) < 0) { - if (channel->duration_seconds > 0.0) { - seek_seconds = 0.0; - avformat_seek_file(format_context, video_stream_index, INT64_MIN, 0, INT64_MAX, 0); - avcodec_flush_buffers(codec_context); - if (audio_codec_context) { - avcodec_flush_buffers(audio_codec_context); - } - player_clear_audio(player); - continue; + 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) { + goto cleanup; } - break; + continue; } - if (packet->stream_index == audio_stream_index && audio_codec_context) { - if (avcodec_send_packet(audio_codec_context, packet) >= 0) { + if (ctx.packet->stream_index == ctx.audio_stream_index && ctx.audio_codec_context) { + if (avcodec_send_packet(ctx.audio_codec_context, ctx.packet) >= 0) { while (!should_stop(player)) { - int receive_audio = avcodec_receive_frame(audio_codec_context, audio_frame); + int receive_audio = avcodec_receive_frame(ctx.audio_codec_context, ctx.audio_frame); if (receive_audio == AVERROR(EAGAIN) || receive_audio == AVERROR_EOF) { break; } if (receive_audio < 0) { player_set_error(player, "Audio decode failed"); - av_packet_unref(packet); + av_packet_unref(ctx.packet); goto cleanup; } - if (queue_audio_frame(player, &swr_context, audio_codec_context, audio_time_base, audio_frame, &queued_any_audio) != 0) { - av_packet_unref(packet); + if (queue_audio_frame(player, + &ctx.swr_context, + ctx.audio_codec_context, + ctx.audio_time_base, + ctx.audio_frame, + program->start_offset_seconds, + &queued_any_audio) != 0) { + av_packet_unref(ctx.packet); goto cleanup; } MAYBE_MARK_PREROLL_READY(); - av_frame_unref(audio_frame); + av_frame_unref(ctx.audio_frame); } } - av_packet_unref(packet); + av_packet_unref(ctx.packet); continue; } - if (packet->stream_index != video_stream_index) { - av_packet_unref(packet); + if (ctx.packet->stream_index != ctx.video_stream_index) { + av_packet_unref(ctx.packet); continue; } - - if (avcodec_send_packet(codec_context, packet) < 0) { - av_packet_unref(packet); + if (avcodec_send_packet(ctx.video_codec_context, ctx.packet) < 0) { + av_packet_unref(ctx.packet); continue; } - av_packet_unref(packet); + av_packet_unref(ctx.packet); while (!should_stop(player)) { double frame_seconds; - int receive = avcodec_receive_frame(codec_context, decoded_frame); + int receive = avcodec_receive_frame(ctx.video_codec_context, ctx.decoded_frame); if (receive == AVERROR(EAGAIN) || receive == AVERROR_EOF) { break; } @@ -492,9 +517,9 @@ static int decode_thread_main(void *userdata) { goto cleanup; } - frame_seconds = decoded_frame->best_effort_timestamp == AV_NOPTS_VALUE - ? seek_seconds - : decoded_frame->best_effort_timestamp * av_q2d(time_base); + frame_seconds = ctx.decoded_frame->best_effort_timestamp == AV_NOPTS_VALUE + ? program->start_offset_seconds + seek_seconds + : program->start_offset_seconds + (ctx.decoded_frame->best_effort_timestamp * av_q2d(ctx.video_time_base)); { FrameData frame = {0}; @@ -502,20 +527,14 @@ static int decode_thread_main(void *userdata) { int dest_linesize[4] = {0}; int image_size; - frame.width = codec_context->width; - frame.height = codec_context->height; + frame.width = ctx.video_codec_context->width; + frame.height = ctx.video_codec_context->height; frame.pts_seconds = frame_seconds; - image_size = av_image_alloc(dest_data, - dest_linesize, - frame.width, - frame.height, - AV_PIX_FMT_YUV420P, - 1); + image_size = av_image_alloc(dest_data, dest_linesize, frame.width, frame.height, AV_PIX_FMT_YUV420P, 1); if (image_size < 0) { player_set_error(player, "Unable to allocate frame buffer"); goto cleanup; } - frame.buffer = dest_data[0]; frame.plane_y = dest_data[0]; frame.plane_u = dest_data[1]; @@ -523,18 +542,15 @@ static int decode_thread_main(void *userdata) { frame.pitch_y = dest_linesize[0]; frame.pitch_u = dest_linesize[1]; frame.pitch_v = dest_linesize[2]; - - sws_scale(sws_context, - (const uint8_t *const *) decoded_frame->data, - decoded_frame->linesize, + sws_scale(ctx.sws_context, + (const uint8_t *const *) ctx.decoded_frame->data, + ctx.decoded_frame->linesize, 0, - codec_context->height, + ctx.video_codec_context->height, dest_data, dest_linesize); frame_queue_push(&player->frame_queue, &frame); - if (!queued_first_video) { - queued_first_video = 1; - } + queued_first_video = 1; MAYBE_MARK_PREROLL_READY(); } } @@ -543,30 +559,7 @@ static int decode_thread_main(void *userdata) { rc = 0; cleanup: - if (packet) { - av_packet_free(&packet); - } - if (decoded_frame) { - av_frame_free(&decoded_frame); - } - if (audio_frame) { - av_frame_free(&audio_frame); - } - if (codec_context) { - avcodec_free_context(&codec_context); - } - if (audio_codec_context) { - avcodec_free_context(&audio_codec_context); - } - if (format_context) { - avformat_close_input(&format_context); - } - if (sws_context) { - sws_freeContext(sws_context); - } - if (swr_context) { - swr_free(&swr_context); - } + decode_context_reset(&ctx); player->has_audio_stream = 0; return rc; diff --git a/src/player.o b/src/player.o index fd2c547ac638ebc75c9a06d61effa1334d6d0419..54c9795295bfa257d4b4ca9eecaf7d55f741a81a 100644 GIT binary patch literal 21296 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 J#b;^#{{&`p7BK(- literal 21808 zcmbt*3z$^ZmF}UNM+2r#V>E+tj5SG_X&h<05fxCh3##F?X%Gbz1#OD%DrxCQrK(y% zP3&0J$Vt(N6CIP$nIS{w4n}i_d@)`OM4N{+WHL&aJCVe>$~a7wCxRAEQeBJ9n_vjUD&;&n5Ca$EqLj z{Gui;>{Q)z`>K}TRwTh@PnGv3-3#-qJA0GKT3dE6Y*0hBMwdW>+zTl%cOXf2p5;cw ze^J$9g{>P<8_e;S8hVADY*vkpeaCtx^*64anQH))8SATj`EAh^d)*6rLG0KZ`nvfC z0VtIVFRTVVZE&;VQ`i|pz3)uVB5-Gu6oh1T!+^ouTF@|ggSosZE?0_M9hXz}m|>*Z zKTqiupSQoYx!fMno1-^HmnwsN?bpXa*<854s=08bHyDeo)g$F2y(}PkgZan?T~y`G zqWn!!-XhA|Oj#?^53cnG>kl{e-yQS(LC=pxs#HtgfV<)KvuNe}Kyj*s9UDyyy$aqX zeFxqAMu_MYQguH*gH%5d-NpRvb4&9b_4j$Xfl=Pf1KB_E3g_(i3O804&h9&${Z;Ox zQ9Z}{-f_L-hM?zlOryJD7C1Jw{;O9Q{hC*3s4lD>tp;!MU-TeU{wWIJ4{Gs!Z3{}y zw!gs?DB;kt4h)V!vg(rP|E5}ZMS5X@`oqosyQP*vv_O&#uPbGsoY(qKp5Gj)Hhd09 zLDWb;B&D|3WGzaeIWied9z@!J0U)lMpEl9T17?$({r5aj38qFkqLXRxBjZTFOEIWF z?D;R1Bz#`AP%_D(;Drkf5%9hQ^Hx)=gHQ|lPGzrYDm<^&Aslv^Vv%&a^$On68BTWh zbhSBAchY(;<|cprerCFdQRSHkJ!vPWva$Xh4=g|B_3u)krpWDzDF4zpizR#uDvrik zEaAPV7@WltzF`Pw(co}8Cp&@8r`>Tu-#ghV7_iiB+fX+a6&@X2-wR!sysz!!;_;7g z4~X6ty*=839RTYP%czC~$S+~8I&#v6lt5)msl{qa5*o$x_q&e_cz{XcxR30qI^_2K zw+giMopSSk#R8Xy^}xb1HAS&V7~3RzZBV@)9q?B~B8~M2qrMW^D`-b9q}CSJW(%7) zg6dBEi~1-04^gPlV&?XR?J~$$8QGf(&(46-&HfqB-=oM*M&0U%yh)C8(w!8GOisG* z4J6%554aDW#?oo(UmpfJ)UTr?$UWv<_|-B0=mP)!sEs)6e7KnupB~2N8dyp4sYQJlAEkh|qdVN%-Ii$OqNJK-Rv4yrF{wsy zN|QY_&7;N+&B+4)SH-JAS=x`aeL%6G@ZY&dsFAU{-2Ay9!wQ2rhl%Drx2h+ZOr$eb zoF;!0=ji`xZi)jR(}KGq9161V2%SERy=?@$e1sd4n38yL z6J64W+bOWwzb`Vw+X)(`-WVL$i}~3|bF7e!)cGBehT<+vgg&3?P22*8vCWRY)8Ghe^+Dxd1L(mn~tIweU7xA z|1Q=&u@k7dQ#LB{-@0G*vIGYJCf(F8{v+vr^(T0KLGX|Qf4)(TfE^+qnG9CSU{vk; z|4q~86{Grt3;Yk^5@3j_KtHeWNTioJu8IIRY7jdkB}bx*Z4I zDf!50)Ir68$a@RPAS?%6>h&*bz^1AM@cN%onl$@ws`{Tv9!6bt5?-&4Wm)GUz|r;!vbIFpDK0`MdrQEuZhn&;h!gl5*oB_o00)N=vXRw8 zM$)_ZZZ!?hsA;f#ln~0@x1!a!`!`V}%a^NY>8I7%g##l?8_{3sn@r7EJ6n+BO~K7S z2V$k`b`+X$;;n|TtryGzLo zH9A17nwoNhRpz~@Y35+hile_yl0%(e}QIOx{uZB&i&7Guk-w041bZ*jl=F@ z;@VX_g^4!jC_TFY&aO5yOtB=JtsDf$H2l2&V9Y-?l8>vQhV6FN7o;n{dHf$w!rL(G zlc`AYjo+y&q27l79bPVvwVSOUTJKe4Wv7^Jdr1SB)I&XP+1=qQ}b49vUZGuf(za{($IvEIVzAjmi}AoOAQuh$2U^a41_xq7Ck* zdz22p;l{|lZ*=CRe};P3_HYy&#Y8mu1IBae2UMrx9k}Db_v&9QHlv&MOptHJV9$rq zhj+YXxFnh$_Eh;1+v-o@AW@9==Og<8R8Tf9-RO?$UTmK+KOfl#Jo=o2TO@QCK&2Ol zD_YDs0FZ|Zlj78f(;@~AjeUpR{52?8mRiexy$?{T8BzDyd}JX=xL3_+#Lylp{(iKr z)YO2sfw9`}@>qjj>!ZuCnmzyLp1&lrND(!)el_J4P{Z}I z%tLPeK2X_1OdVb|>Xf?V6E_QW>t7j#@o7WYe$#YxEm=5^E$s_}mxoQHI_kd!ZhP>k z%gIT1*#Isn>Xq@Nd&!Zv!=?M9Qf1~5chk!-*9%ZY+txF$W>J`hbB@jRUt<1m>`lV| ziC3soUAf87e*RulwFd8d)$RLd@S~HYz(F@yk!P^(&1Nt3KQ5Rpqo@s@L;Kez(5m?g{ zKZgp0mVF593fWdUxEBioG5)ORiV&FM_uGWpk-<- zgv7%1m&X882*rZu_<8_}g01UBYcYhy?U@Brk9YEcX8$8NdHpH1dQ=~~xT097W(I1p zkLg9Gj6`=($O*`)kY(0v#d~r2S8T9_l-Tjo?)iJwn#=Y|LoDjXD#MIzsMt$#r4RiP5Mbz!P9mQzj34P6jC$ykmT!v zfXTU`=^Zgl--@V1_8ax1% zqHj*m8J{o(WnP0;Z6Hhs&+z9iQbH(pxI6szJDdnwPEi%FD(5z$bzNpVeA-c4={~rdhcSe^q_uo;qwEi6xn)Q$KcEp(U zATD5P>3X?0Pd5!V(tRCo%7~h|n1Rte{|7b526J`z<)vO}<@veQ zRuq&|rE=jL#7!U>Eu1zFvB{ad99{%dX2YKUy5fO%k47EKW;|&qp6&Us=rMFOntk+Z z0Jv&)7T7^~-rhMvVA(Pty(+^kJ%$me5Tc_}>yP^jLt4N6RPMkT89`2NR`WlIo(!l@ zCJzq*^^3j!7Z~;#3Oz#%wwlN;!qDrh)%}A-w*3 zF{u=dpAH6d+cAqwRMeX?Ou^-QZQNdnqKZ5U2Xox$nA}M@4@G3lkQEoh(&nD5>~TNc zY{^ou^<5qHe~?bXfbT-(h&T}&QJA^9%H4D|5o5XL{=cWuvR_nY2VH$! z{eb#bB2?s9l;Kh(t6kPc2-ul2Srk=Hr=rwe{(U^QH!s*g6$gn+7z9K&TJ)s6MXy#5uDu;;&K?>DsdlVAhF=1A2^cLn14 zDC6wWA=dASwq=kQ=MLQd@X!p@z1GuS`x!d5S2@e`PvYB%_8$r= zJxERZIG8bdll`h$U3iRZkZ&QcL5RYG`g-K%53%+bHxAyC_n_uJ`yk&!ct_Ur!_c|9 zs2?l8A7f^ZYX5X|527nRou(+LKV8MO=pm1<1yFb@Pgoxh$tL%!uPOh-N7cggNOqCl zT@{4Ju9v6}o>Tq+Lxu4B+wnyXes%zdPc^3Q_M}_0`=q;P|Cpq^g0E7aR^2}z!2bEj9v+F~ z4=umVeRcrs;BV#Yne#ih>|5)Eg4z^uq>f*1{ZR2P9X01+@YNXBeoJjAeZN(7*s@;J zwD$ZG--3&rYM(M+ z!tFy%lGS$IX&Gl}j$|TGQ2icUL&o5ns0|-nu57NT<8g&P5p~ z+SAtF9Y%m3L4JE5yC@TmE4aO@J!`;OVGwJww3F)T$RrerlWI>T?ru*foOnkj4#k+3 zcEwk9B*NM5Fd4MRJKFC_gyUW7!i!_ghI3oG{Vv1}byA#Wg?lpXU3Ywf2IEdJbR+Xs zlBN@xcxS3ZvmBy}cXV{O#%OF4VKAdk#~kW6WMrMJR5fwc8Bk3Z%cHC zO~cZzOiwD+oz5oORKpPelHBUsOSrYWE1OuG9ctHhwWhVx0zLJ-8Ng(=yQAk}Xu?51 zZ(e6AaYs0rj&~-)rMb$q#%anDKmE1E_=+`kIC+y&Mvp=_0lj8q=!fLZWaH^<*tl8R z%!cF4pF4L}xF(iZ)gJE(*H5pVR$DiS?=#S%q7CsgI_-Tza^iGXAS>5v}g&vl{dTt&q#6ZVFx7EFvz2vyCW zxGYq)mo(eij`T|kJ||TDP{o|k&HezGjikRhqjEaoLxB~ zG-Gb4E*h!<4QSB^1J9dS@pNb#dTbv(KePk+!00BD`=fzYcz|z1Axh}UDwJ*j(gec! zAY2I4X~6KjGcqk^z9dxLU(pnrys@$|6kb1iZm1?VW#dgSXSR@@I= z_W$&soG`le8Xn`BYrS6#RYmuDGy$OLc)nw3_%|2MmrAyb(Uz6xjp9@&rJ@rnmMhjE ztH$%Mrr)PgX1W^B34^`K91``@ej7P2>!B`Wo*z0is`5rKtpWd%)E@q#LGi4kP)LnB zQ+9|M>|7(?CNbCZVSclWq%E(bUn*NJC|R;`V%i#& zbblaG<&WuD-=@?^|HC8%f$`Hrl{#zmF)tUo#zQKEuGpY52oYz&-pCE{&)(vtG z4{@$xCbdqGFUK>=QE$|Q9TnDBCC8~0xPhE*RrsWw>LU>3|07Fi@^vbo;53xs_Zxg= z8Qy2`R2g0{cyAf5^_$>qF2f%)^x-o6uMA#ShHo|aqB8tRgC8ox|H0tlvZ?%`!3WCd z5gA9qcjB@%pz0Hxqh&bvOV0ill@I-3TSS^r+V5=3VT0Sc&5szo+5#r$&6j+t%j7?% z@G(xU%7=c`RmK_bU|9}J@2cAPVHhjFRmfPU+|H*JKF0a1%7=b_Z|Ii|L8?@t9<^mS!?K5OuXGF;E=Sf{s)&leTl_+jgWB$pVx+|JX0mt&;o zcq8FW0r)KecrpOLHvpdn7aVC^?h7N~JYz<}83Gvz|K|YwtpL0V`|e15J`;dn6o6kA zfO`S>Z2@>^0De~h-WPyB8i0Q%0RKS%{<8r5^#J^x0Q~m>_$0iljAZ{m4ZyDmz!|9; ziO&rIcv}E|PXPW%0RFcDIA6g=lIO<(_<;cYjR5>a0DdL_|NQ8Y<*5z8n*#8a0r;H( z___f6fdKsR0Q|cF`11kyU;zGV0RBb*{uc1FvH7Py!(G$dsga)wpdX8?$w>45M*(;Z zaPmK7E-vc64gOaK&^HF)O9JrK0r*#dbKIlm!lmx;7}pP=e>?#H+W`CrhJVT&wDzw5 zasd6W4E<(9uigV7Pes6aab5s^X#jpLaO&G@ULG64n`w!`4;fs&D*(UK;3qCnWRAW6 z=M3(APUGqw0L5<^e5JwFeINMuEFXif(%l?~;h03aw$&6`GKp-9j?Czmmd<#3O-m}B zNOyO1v@oc!&XTrvB;x579fQ$t;4M1Ffj;eBj*hRiw8p#I+7Ww5WNPc2+UB|Hr=F$y zE7b1{^*fWl(^TU$-8x<2m#g0^RjVprrfF2`WeQeorcG02h19EZy>3;ERex1d{ZtRd zO$|Ox|0<%(RIkf4dp)4ycA0Ly!a)G3rMbIx&4M1pL!3-kD&5|dO)7Y47eYOzfn@U& z*(L3*Yci^FF0FM#PogK$7G?ZHfs5ijnS`pw5_h$?B7}8UvOC=w&$ckI)6(9R>d6ja z$@Z?c79F@jQ?eT^Dmc@k8ce9A1gZc|i;3G@R)SM;23oTC%hi@_w{0%9a@c98wWJWD zgDWJ{2?~WAoovkviYC$D$lcn}ok+E+&B5S)z zQ(iIMXPmF9bRc}!lIqSNWYpb74{dGFB$QR7+3wEvR#=~$>9Tq>F-}icI*~+O zYm!O8u3F&|26hP!pcH4%_RjbniIyPℜLTcKI@=wI$V)Sq(~Kh)g1}1`~sroE;|F z0fH9$XTwITxjpH0qARO?Ku09mqUucuU228sFgNs=8Wd|(u~QOuCOWZXDPYMi@pauj z*_O6MGTzgXRYr#~QO8Or9fD_!t+k}_-yKlZLCcKRtR#DrkN!53RNrbec)s#SYGMy7A2Mv%Oxi?KyEXzS|9qVs5%<`&hR%4Vi1 z*%U3AUk}%nODU7ZpM|EKq*U?Rt9w!{Jzd%M4%^V4!LHG=sssBD+}EOTRw@}+JEqZ`jP()a*y9f|CLLGB-|MKv z_!twe`zm&1@mUJhIAeAeZ!$Rf@b8%{uHz?+VL8sF0{=UdnaaoAqhWPg*;RNf{lfwHR)I54ZRzbF ziLjY}*Jbeog8mwTkHYNXTj|%+90-qyr-nW@J?p!=}-1BiS zWg;HKWAQ5m-YD=^fzK280|J-x`*nkJem^DXpA>jV;Li#^a^Bw(xa5CQ@Nos73KL4^ zxO@+@dQCGp^_6^P3HnKb{yKr5E%18+_&gx!`L1KfeOk~_KqAhw z!0P1-{5pYuOW4w(Nqe3Y_(HT<{u4OaNR)?DVe!uzoP4eq_~im$B=CkZ zKJ-PVxWF08u>8}4kF-xt&{IWA|B#@cD)7ezF8y$uz~#KW9Dsi$@Mgh(TnH5;+F=16 zE6;fW_zeP=dfj61@_FwOI72R${{wEhNfw6CTSaD)5^Ho)kF0by@lgc*r4felNsh@p^-k|1AQaEAU$d{w0CO1l}QV ze)F~b?-lq8f%gUAe<^Tj=YJHqwDZ3R{5HYAl7}=B^|~F8m1nBKX@{`Dy9IrVz#kU) zN`b#9@VLM$%!bKvrJQF4;O7Wj`uS9WOFLX?aLUgk(dxBA&`Ucf1Mrl6ll7w}m7EEn{0 zUOELX`KJXg=cOQUIqu^IFQ1p~f?m$cj{@+00+;gqQs8pjHw?~kBSPN^ynG;)`{5r8 zT=JhLaLIp`!O35qM~el15!$SsXpP}$Bha64uLNX zz>@*^x&XW{0Dm|De_!D3LY@lqJ%;k%De%t;T-txSz@U+JwyU@qZ;~IfWK6eTHQbGUM0+)9CzQCp3b_%>( z@Oi`F^z$zY{FIiZHSNK&LZPuPU#eB)Q z)0P1F7YcbKeUanZT{Vn z#O?R(jb*s~zP+Ukw*mg2mf`mM_WNb{2{W%wZDw6(HogjZHm&p3wv@*As9OFpyHl(F zCcbu6CgaqmyICQ5eJwsf%PKw(t3HYVB{bIJn?}5&MAn+gl1Q^ag`c)tyE{AaWzVVY z>dq!==Pz736@P+`4~PYA|)1 z9luhSR-1c{iz>&les7Lhbruol+K}nb%f_{B#*J)M?RDh&|Jt8>7J2fFlgW<1T7oFs z@onB$^MHLY6{N~(*EP{ z*zql2-k~`D*?45y5!N-6m6~jSoAXQ@ss9nvaSI4moHJ{f&SZAeQ z%g*KnwAgZq2nEmdml(_Cv|9{T!A6m6|C+Liv3gt5$59x`{@*nAUu_1Y{rP@j`&;|7 zxqNIuPKqxbNIQ7wA<duration_seconds; + program = channel_resolve_program(selected_channel, app_start_ticks, now_ticks, &program_seek, NULL); + if (!program) { + return; + } + start_time = now_wall - (time_t) program_seek; + end_time = start_time + (time_t) program->duration_seconds; format_time_compact(time_range, sizeof(time_range), start_time); format_time_compact(end_text, sizeof(end_text), end_time); strncat(time_range, " - ", sizeof(time_range) - strlen(time_range) - 1); strncat(time_range, end_text, sizeof(time_range) - strlen(time_range) - 1); snprintf(description, sizeof(description), - "Channel %d presents %s. Source: %s.", - selected_channel->number, - selected_channel->program_title, - selected_channel->file_name); + "%s", + selected_channel->description[0] != '\0' ? selected_channel->description : "Local programming lineup."); clip_rect = (SDL_Rect){rect->x + 16, rect->y + 12, rect->w - 32, rect->h - 24}; draw_text_clipped(renderer, fonts->large, selected_channel->name, &clip_rect, rect->x + 18, rect->y + 44, COLOR_PANEL_TEXT); - draw_text_clipped(renderer, fonts->medium, selected_channel->program_title, &clip_rect, rect->x + 18, rect->y + 88, COLOR_PANEL_TEXT); + draw_text_clipped(renderer, fonts->medium, program->program_title, &clip_rect, rect->x + 18, rect->y + 88, COLOR_PANEL_TEXT); draw_text_clipped(renderer, fonts->small, time_range, &clip_rect, rect->x + 18, rect->y + 124, COLOR_PANEL_TEXT); draw_text_clipped(renderer, fonts->small, description, &clip_rect, rect->x + 18, rect->y + 148, COLOR_PANEL_TEXT); } @@ -581,15 +583,27 @@ void ui_render_guide(SDL_Renderer *renderer, { const Channel *channel = &channels->items[channel_index]; - double live_position = channel_live_position_precise(channel, app_start_ticks, now_ticks); + 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; - int block_x = GUIDE_X_START + (int) (((double) (program_start_time - guide_view_start_time)) / 60.0 * pixels_per_minute); - int block_w = (int) ((channel->duration_seconds / 60.0) * pixels_per_minute); - SDL_Rect block = {block_x, timeline_rect.y + 4, SDL_max(block_w, 48), timeline_rect.h - 8}; + 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}; SDL_Rect title_rect = {block.x + 8, block.y + 8, block.w - 16, block.h - 16}; char title[128]; + 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); + 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}; + if (is_selected) { draw_text_clipped(renderer, fonts->medium, channel->name, &sidebar, 20, row_rect.y + 12, COLOR_ACTIVE_TEXT); { @@ -601,7 +615,7 @@ void ui_render_guide(SDL_Renderer *renderer, 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); + draw_text_clipped(renderer, fonts->small, program->file_name, &sidebar, 20, row_rect.y + 38, COLOR_PALE_BLUE); SDL_RenderSetClipRect(renderer, &clip); draw_beveled_bar(renderer, @@ -612,7 +626,7 @@ void ui_render_guide(SDL_Renderer *renderer, is_selected ? COLOR_SELECTION_EDGE : COLOR_GLOSS, COLOR_BORDER_DARK); fit_text_with_ellipsis(is_selected ? fonts->medium : fonts->small, - channel->program_title, + program->program_title, title_rect.w, title, sizeof(title)); @@ -676,17 +690,18 @@ int ui_cache_init(UiCache *cache, SDL_Renderer *renderer, const UiFonts *fonts, for (int i = 0; i < channels->count; ++i) { const Channel *channel = &channels->items[i]; + const ProgramEntry *program = channel->program_count > 0 ? &channel->programs[0] : NULL; 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) { + text_texture_init(&cache->channels[i].file_small, renderer, fonts->small, program ? program->file_name : "", COLOR_PALE_BLUE) != 0 || + text_texture_init(&cache->channels[i].program_small_light, renderer, fonts->small, program ? program->program_title : "", COLOR_TEXT_LIGHT) != 0 || + text_texture_init(&cache->channels[i].program_medium_dark, renderer, fonts->medium, program ? program->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); + snprintf(buffer, sizeof(buffer), "%.0f min - %s", program ? program->duration_seconds / 60.0 : 0.0, program ? program->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; diff --git a/src/ui.o b/src/ui.o index 4a20601d45ac95169c187b17f8aed5d2d9ef195e..b930071afba05a510e6072a9f8d7e429ae711475 100644 GIT binary patch literal 25328 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 literal 24768 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

$D8Kphttf$UBS{FVx;uG=fKsZ$%Tc3 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 SQ`I+VQAPNL9VLI$>;FF#;slQX