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 619064d..88915cf 100644 Binary files a/src/channel.o and b/src/channel.o differ 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 fd2c547..54c9795 100644 Binary files a/src/player.o and b/src/player.o differ diff --git a/src/ui.c b/src/ui.c index e612efb..d9d9bc3 100644 --- a/src/ui.c +++ b/src/ui.c @@ -389,7 +389,8 @@ static void draw_info_panel(SDL_Renderer *renderer, char description[384]; time_t start_time; time_t end_time; - double live_position; + double program_seek; + const ProgramEntry *program; if (!rect || !selected_channel) { return; @@ -402,23 +403,24 @@ static void draw_info_panel(SDL_Renderer *renderer, draw_panel_bevel(renderer, rect); stroke_rect(renderer, rect, COLOR_BORDER_DARK); - live_position = channel_live_position_precise(selected_channel, app_start_ticks, now_ticks); - start_time = now_wall - (time_t) live_position; - end_time = start_time + (time_t) selected_channel->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 4a20601..b930071 100644 Binary files a/src/ui.o and b/src/ui.o differ