New directory based system for channels
This commit is contained in:
parent
3e6d29670c
commit
b84a3060c1
8 changed files with 466 additions and 241 deletions
25
README.md
25
README.md
|
|
@ -42,12 +42,29 @@ make
|
||||||
|
|
||||||
## Media
|
## 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`
|
```txt
|
||||||
- `media/computers.mp4`
|
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
|
## Fonts
|
||||||
|
|
||||||
|
|
|
||||||
256
src/channel.c
256
src/channel.c
|
|
@ -8,6 +8,7 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <strings.h>
|
#include <strings.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
static int has_supported_extension(const char *name) {
|
static int has_supported_extension(const char *name) {
|
||||||
const char *dot = strrchr(name, '.');
|
const char *dot = strrchr(name, '.');
|
||||||
|
|
@ -23,6 +24,11 @@ static int has_supported_extension(const char *name) {
|
||||||
strcasecmp(dot, "m4v") == 0;
|
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) {
|
static void trim_extension(char *text) {
|
||||||
char *dot = strrchr(text, '.');
|
char *dot = strrchr(text, '.');
|
||||||
if (dot) {
|
if (dot) {
|
||||||
|
|
@ -38,42 +44,45 @@ static void make_title_case(char *text) {
|
||||||
new_word = 1;
|
new_word = 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isspace((unsigned char) text[i])) {
|
if (isspace((unsigned char) text[i])) {
|
||||||
new_word = 1;
|
new_word = 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
text[i] = new_word ? (char) toupper((unsigned char) text[i]) : (char) tolower((unsigned char) text[i]);
|
text[i] = new_word ? (char) toupper((unsigned char) text[i]) : (char) tolower((unsigned char) text[i]);
|
||||||
new_word = 0;
|
new_word = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void derive_channel_name(const char *file_name, char *out, size_t out_size) {
|
static void safe_copy(char *out, size_t out_size, const char *value) {
|
||||||
char temp[128];
|
if (!out || out_size == 0) {
|
||||||
snprintf(temp, sizeof(temp), "%s", file_name);
|
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);
|
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);
|
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) {
|
static void derive_program_title(const char *file_name, char *out, size_t out_size) {
|
||||||
char temp[128];
|
derive_display_name(file_name, out, out_size);
|
||||||
snprintf(temp, sizeof(temp), "%s", file_name);
|
}
|
||||||
trim_extension(temp);
|
|
||||||
make_title_case(temp);
|
static char *trim_whitespace(char *text) {
|
||||||
snprintf(out, out_size, "%s", temp);
|
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) {
|
static double probe_duration_seconds(const char *file_path) {
|
||||||
|
|
@ -92,10 +101,126 @@ static double probe_duration_seconds(const char *file_path) {
|
||||||
return duration;
|
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) {
|
static int compare_channels(const void *left, const void *right) {
|
||||||
const Channel *a = (const Channel *) left;
|
const Channel *a = (const Channel *) left;
|
||||||
const Channel *b = (const Channel *) right;
|
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) {
|
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) {
|
while ((entry = readdir(directory)) != NULL) {
|
||||||
Channel *channel;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,12 +273,20 @@ int channel_list_load(ChannelList *list, const char *media_dir) {
|
||||||
|
|
||||||
channel = &list->items[list->count];
|
channel = &list->items[list->count];
|
||||||
memset(channel, 0, sizeof(*channel));
|
memset(channel, 0, sizeof(*channel));
|
||||||
channel->number = 50 + list->count;
|
derive_display_name(entry->d_name, channel->name, sizeof(channel->name));
|
||||||
snprintf(channel->file_name, sizeof(channel->file_name), "%s", entry->d_name);
|
safe_copy(channel->description, sizeof(channel->description), "Local programming lineup.");
|
||||||
snprintf(channel->file_path, sizeof(channel->file_path), "%s/%s", media_dir, entry->d_name);
|
parse_channel_metadata(channel, channel_dir);
|
||||||
derive_channel_name(entry->d_name, channel->name, sizeof(channel->name));
|
load_result = load_channel_programs(channel, channel_dir);
|
||||||
derive_program_title(entry->d_name, channel->program_title, sizeof(channel->program_title));
|
if (load_result < 0) {
|
||||||
channel->duration_seconds = probe_duration_seconds(channel->file_path);
|
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;
|
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);
|
qsort(list->items, (size_t) list->count, sizeof(Channel), compare_channels);
|
||||||
for (int i = 0; i < list->count; ++i) {
|
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;
|
return list->count;
|
||||||
|
|
@ -170,6 +312,11 @@ void channel_list_destroy(ChannelList *list) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (list->items) {
|
||||||
|
for (int i = 0; i < list->count; ++i) {
|
||||||
|
free(list->items[i].programs);
|
||||||
|
}
|
||||||
|
}
|
||||||
free(list->items);
|
free(list->items);
|
||||||
list->items = NULL;
|
list->items = NULL;
|
||||||
list->count = 0;
|
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 channel_live_position(const Channel *channel, time_t app_start_time, time_t now) {
|
||||||
double elapsed;
|
double elapsed;
|
||||||
|
|
||||||
if (!channel || channel->duration_seconds <= 0.0) {
|
if (!channel || channel->total_duration_seconds <= 0.0) {
|
||||||
return 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;
|
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 channel_live_position_precise(const Channel *channel, Uint64 app_start_ticks, Uint64 now_ticks) {
|
||||||
double elapsed;
|
double elapsed;
|
||||||
|
|
||||||
if (!channel || channel->duration_seconds <= 0.0) {
|
if (!channel || channel->total_duration_seconds <= 0.0) {
|
||||||
return 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;
|
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];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,19 @@
|
||||||
typedef struct Channel {
|
typedef struct Channel {
|
||||||
int number;
|
int number;
|
||||||
char name[64];
|
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_path[PATH_MAX];
|
||||||
char file_name[128];
|
char file_name[128];
|
||||||
char program_title[128];
|
char program_title[128];
|
||||||
double duration_seconds;
|
double duration_seconds;
|
||||||
} Channel;
|
double start_offset_seconds;
|
||||||
|
} ProgramEntry;
|
||||||
|
|
||||||
typedef struct ChannelList {
|
typedef struct ChannelList {
|
||||||
Channel *items;
|
Channel *items;
|
||||||
|
|
@ -23,5 +31,11 @@ int channel_list_load(ChannelList *list, const char *media_dir);
|
||||||
void channel_list_destroy(ChannelList *list);
|
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(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,
|
||||||
|
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
|
#endif
|
||||||
|
|
|
||||||
BIN
src/channel.o
BIN
src/channel.o
Binary file not shown.
357
src/player.c
357
src/player.c
|
|
@ -184,6 +184,7 @@ static int queue_audio_frame(Player *player,
|
||||||
AVCodecContext *audio_codec_context,
|
AVCodecContext *audio_codec_context,
|
||||||
AVRational audio_time_base,
|
AVRational audio_time_base,
|
||||||
AVFrame *audio_frame,
|
AVFrame *audio_frame,
|
||||||
|
double pts_base_offset,
|
||||||
int *queued_any_audio) {
|
int *queued_any_audio) {
|
||||||
uint8_t **converted_data = NULL;
|
uint8_t **converted_data = NULL;
|
||||||
int out_rate;
|
int out_rate;
|
||||||
|
|
@ -269,7 +270,7 @@ static int queue_audio_frame(Player *player,
|
||||||
|
|
||||||
audio_pts = audio_frame->best_effort_timestamp == AV_NOPTS_VALUE
|
audio_pts = audio_frame->best_effort_timestamp == AV_NOPTS_VALUE
|
||||||
? NAN
|
? 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;
|
frame_duration = (double) sample_count / (double) out_rate;
|
||||||
|
|
||||||
queued_limit = (int) (player_audio_bytes_per_second(player) * 0.18);
|
queued_limit = (int) (player_audio_bytes_per_second(player) * 0.18);
|
||||||
|
|
@ -303,187 +304,211 @@ static int queue_audio_frame(Player *player,
|
||||||
return 0;
|
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) {
|
static int decode_thread_main(void *userdata) {
|
||||||
DecoderThreadArgs *args = (DecoderThreadArgs *) userdata;
|
DecoderThreadArgs *args = (DecoderThreadArgs *) userdata;
|
||||||
Player *player = args->player;
|
Player *player = args->player;
|
||||||
const Channel *channel = args->channel;
|
const Channel *channel = args->channel;
|
||||||
AVFormatContext *format_context = NULL;
|
DecodeContext ctx;
|
||||||
AVCodecContext *codec_context = NULL;
|
const ProgramEntry *program;
|
||||||
AVCodecContext *audio_codec_context = NULL;
|
int current_program_index = 0;
|
||||||
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;
|
|
||||||
double seek_seconds = 0.0;
|
double seek_seconds = 0.0;
|
||||||
AVRational time_base;
|
|
||||||
AVRational audio_time_base = {0};
|
|
||||||
int queued_any_audio = 0;
|
int queued_any_audio = 0;
|
||||||
int queued_first_video = 0;
|
int queued_first_video = 0;
|
||||||
|
int rc = -1;
|
||||||
|
|
||||||
#define MAYBE_MARK_PREROLL_READY() \
|
#define MAYBE_MARK_PREROLL_READY() do { if (queued_first_video && (queued_any_audio || !player->has_audio_stream)) { player_mark_preroll_ready(player); } } while (0)
|
||||||
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);
|
free(args);
|
||||||
|
|
||||||
if (avformat_open_input(&format_context, channel->file_path, NULL, NULL) < 0) {
|
program = channel_resolve_program(channel, player->app_start_ticks, SDL_GetTicks64(), &seek_seconds, ¤t_program_index);
|
||||||
player_set_error(player, "Unable to open media file");
|
if (!program || decode_context_open(&ctx, program, player) != 0) {
|
||||||
|
decode_context_reset(&ctx);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (avformat_find_stream_info(format_context, NULL) < 0) {
|
if (seek_seconds > 0.0) {
|
||||||
player_set_error(player, "Unable to read stream metadata");
|
int64_t seek_target = (int64_t) (seek_seconds / av_q2d(ctx.video_time_base));
|
||||||
goto cleanup;
|
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) {
|
||||||
for (unsigned int i = 0; i < format_context->nb_streams; ++i) {
|
avcodec_flush_buffers(ctx.audio_codec_context);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
player_clear_audio(player);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (!should_stop(player)) {
|
while (!should_stop(player)) {
|
||||||
if (av_read_frame(format_context, packet) < 0) {
|
if (av_read_frame(ctx.format_context, ctx.packet) < 0) {
|
||||||
if (channel->duration_seconds > 0.0) {
|
current_program_index = (current_program_index + 1) % channel->program_count;
|
||||||
seek_seconds = 0.0;
|
program = channel_program_at_index(channel, current_program_index);
|
||||||
avformat_seek_file(format_context, video_stream_index, INT64_MIN, 0, INT64_MAX, 0);
|
queued_any_audio = 0;
|
||||||
avcodec_flush_buffers(codec_context);
|
queued_first_video = 0;
|
||||||
if (audio_codec_context) {
|
if (!program || decode_context_open(&ctx, program, player) != 0) {
|
||||||
avcodec_flush_buffers(audio_codec_context);
|
goto cleanup;
|
||||||
}
|
|
||||||
player_clear_audio(player);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
break;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packet->stream_index == audio_stream_index && audio_codec_context) {
|
if (ctx.packet->stream_index == ctx.audio_stream_index && ctx.audio_codec_context) {
|
||||||
if (avcodec_send_packet(audio_codec_context, packet) >= 0) {
|
if (avcodec_send_packet(ctx.audio_codec_context, ctx.packet) >= 0) {
|
||||||
while (!should_stop(player)) {
|
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) {
|
if (receive_audio == AVERROR(EAGAIN) || receive_audio == AVERROR_EOF) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (receive_audio < 0) {
|
if (receive_audio < 0) {
|
||||||
player_set_error(player, "Audio decode failed");
|
player_set_error(player, "Audio decode failed");
|
||||||
av_packet_unref(packet);
|
av_packet_unref(ctx.packet);
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
if (queue_audio_frame(player, &swr_context, audio_codec_context, audio_time_base, audio_frame, &queued_any_audio) != 0) {
|
if (queue_audio_frame(player,
|
||||||
av_packet_unref(packet);
|
&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;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
MAYBE_MARK_PREROLL_READY();
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packet->stream_index != video_stream_index) {
|
if (ctx.packet->stream_index != ctx.video_stream_index) {
|
||||||
av_packet_unref(packet);
|
av_packet_unref(ctx.packet);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (avcodec_send_packet(ctx.video_codec_context, ctx.packet) < 0) {
|
||||||
if (avcodec_send_packet(codec_context, packet) < 0) {
|
av_packet_unref(ctx.packet);
|
||||||
av_packet_unref(packet);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
av_packet_unref(packet);
|
av_packet_unref(ctx.packet);
|
||||||
|
|
||||||
while (!should_stop(player)) {
|
while (!should_stop(player)) {
|
||||||
double frame_seconds;
|
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) {
|
if (receive == AVERROR(EAGAIN) || receive == AVERROR_EOF) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -492,9 +517,9 @@ static int decode_thread_main(void *userdata) {
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
frame_seconds = decoded_frame->best_effort_timestamp == AV_NOPTS_VALUE
|
frame_seconds = ctx.decoded_frame->best_effort_timestamp == AV_NOPTS_VALUE
|
||||||
? seek_seconds
|
? program->start_offset_seconds + seek_seconds
|
||||||
: decoded_frame->best_effort_timestamp * av_q2d(time_base);
|
: program->start_offset_seconds + (ctx.decoded_frame->best_effort_timestamp * av_q2d(ctx.video_time_base));
|
||||||
|
|
||||||
{
|
{
|
||||||
FrameData frame = {0};
|
FrameData frame = {0};
|
||||||
|
|
@ -502,20 +527,14 @@ static int decode_thread_main(void *userdata) {
|
||||||
int dest_linesize[4] = {0};
|
int dest_linesize[4] = {0};
|
||||||
int image_size;
|
int image_size;
|
||||||
|
|
||||||
frame.width = codec_context->width;
|
frame.width = ctx.video_codec_context->width;
|
||||||
frame.height = codec_context->height;
|
frame.height = ctx.video_codec_context->height;
|
||||||
frame.pts_seconds = frame_seconds;
|
frame.pts_seconds = frame_seconds;
|
||||||
image_size = av_image_alloc(dest_data,
|
image_size = av_image_alloc(dest_data, dest_linesize, frame.width, frame.height, AV_PIX_FMT_YUV420P, 1);
|
||||||
dest_linesize,
|
|
||||||
frame.width,
|
|
||||||
frame.height,
|
|
||||||
AV_PIX_FMT_YUV420P,
|
|
||||||
1);
|
|
||||||
if (image_size < 0) {
|
if (image_size < 0) {
|
||||||
player_set_error(player, "Unable to allocate frame buffer");
|
player_set_error(player, "Unable to allocate frame buffer");
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.buffer = dest_data[0];
|
frame.buffer = dest_data[0];
|
||||||
frame.plane_y = dest_data[0];
|
frame.plane_y = dest_data[0];
|
||||||
frame.plane_u = dest_data[1];
|
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_y = dest_linesize[0];
|
||||||
frame.pitch_u = dest_linesize[1];
|
frame.pitch_u = dest_linesize[1];
|
||||||
frame.pitch_v = dest_linesize[2];
|
frame.pitch_v = dest_linesize[2];
|
||||||
|
sws_scale(ctx.sws_context,
|
||||||
sws_scale(sws_context,
|
(const uint8_t *const *) ctx.decoded_frame->data,
|
||||||
(const uint8_t *const *) decoded_frame->data,
|
ctx.decoded_frame->linesize,
|
||||||
decoded_frame->linesize,
|
|
||||||
0,
|
0,
|
||||||
codec_context->height,
|
ctx.video_codec_context->height,
|
||||||
dest_data,
|
dest_data,
|
||||||
dest_linesize);
|
dest_linesize);
|
||||||
frame_queue_push(&player->frame_queue, &frame);
|
frame_queue_push(&player->frame_queue, &frame);
|
||||||
if (!queued_first_video) {
|
queued_first_video = 1;
|
||||||
queued_first_video = 1;
|
|
||||||
}
|
|
||||||
MAYBE_MARK_PREROLL_READY();
|
MAYBE_MARK_PREROLL_READY();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -543,30 +559,7 @@ static int decode_thread_main(void *userdata) {
|
||||||
rc = 0;
|
rc = 0;
|
||||||
|
|
||||||
cleanup:
|
cleanup:
|
||||||
if (packet) {
|
decode_context_reset(&ctx);
|
||||||
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);
|
|
||||||
}
|
|
||||||
player->has_audio_stream = 0;
|
player->has_audio_stream = 0;
|
||||||
return rc;
|
return rc;
|
||||||
|
|
||||||
|
|
|
||||||
BIN
src/player.o
BIN
src/player.o
Binary file not shown.
53
src/ui.c
53
src/ui.c
|
|
@ -389,7 +389,8 @@ static void draw_info_panel(SDL_Renderer *renderer,
|
||||||
char description[384];
|
char description[384];
|
||||||
time_t start_time;
|
time_t start_time;
|
||||||
time_t end_time;
|
time_t end_time;
|
||||||
double live_position;
|
double program_seek;
|
||||||
|
const ProgramEntry *program;
|
||||||
|
|
||||||
if (!rect || !selected_channel) {
|
if (!rect || !selected_channel) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -402,23 +403,24 @@ static void draw_info_panel(SDL_Renderer *renderer,
|
||||||
draw_panel_bevel(renderer, rect);
|
draw_panel_bevel(renderer, rect);
|
||||||
stroke_rect(renderer, rect, COLOR_BORDER_DARK);
|
stroke_rect(renderer, rect, COLOR_BORDER_DARK);
|
||||||
|
|
||||||
live_position = channel_live_position_precise(selected_channel, app_start_ticks, now_ticks);
|
program = channel_resolve_program(selected_channel, app_start_ticks, now_ticks, &program_seek, NULL);
|
||||||
start_time = now_wall - (time_t) live_position;
|
if (!program) {
|
||||||
end_time = start_time + (time_t) selected_channel->duration_seconds;
|
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(time_range, sizeof(time_range), start_time);
|
||||||
format_time_compact(end_text, sizeof(end_text), end_time);
|
format_time_compact(end_text, sizeof(end_text), end_time);
|
||||||
strncat(time_range, " - ", sizeof(time_range) - strlen(time_range) - 1);
|
strncat(time_range, " - ", sizeof(time_range) - strlen(time_range) - 1);
|
||||||
strncat(time_range, end_text, sizeof(time_range) - strlen(time_range) - 1);
|
strncat(time_range, end_text, sizeof(time_range) - strlen(time_range) - 1);
|
||||||
snprintf(description,
|
snprintf(description,
|
||||||
sizeof(description),
|
sizeof(description),
|
||||||
"Channel %d presents %s. Source: %s.",
|
"%s",
|
||||||
selected_channel->number,
|
selected_channel->description[0] != '\0' ? selected_channel->description : "Local programming lineup.");
|
||||||
selected_channel->program_title,
|
|
||||||
selected_channel->file_name);
|
|
||||||
|
|
||||||
clip_rect = (SDL_Rect){rect->x + 16, rect->y + 12, rect->w - 32, rect->h - 24};
|
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->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, 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);
|
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];
|
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 guide_view_start_time = now_wall - (30 * 60);
|
||||||
time_t program_start_time = now_wall - (time_t) live_position;
|
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_x = GUIDE_X_START;
|
||||||
int block_w = (int) ((channel->duration_seconds / 60.0) * pixels_per_minute);
|
int block_w = 48;
|
||||||
SDL_Rect block = {block_x, timeline_rect.y + 4, SDL_max(block_w, 48), timeline_rect.h - 8};
|
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};
|
SDL_Rect title_rect = {block.x + 8, block.y + 8, block.w - 16, block.h - 16};
|
||||||
char title[128];
|
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) {
|
if (is_selected) {
|
||||||
draw_text_clipped(renderer, fonts->medium, channel->name, &sidebar, 20, row_rect.y + 12, COLOR_ACTIVE_TEXT);
|
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].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].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);
|
SDL_RenderSetClipRect(renderer, &clip);
|
||||||
draw_beveled_bar(renderer,
|
draw_beveled_bar(renderer,
|
||||||
|
|
@ -612,7 +626,7 @@ void ui_render_guide(SDL_Renderer *renderer,
|
||||||
is_selected ? COLOR_SELECTION_EDGE : COLOR_GLOSS,
|
is_selected ? COLOR_SELECTION_EDGE : COLOR_GLOSS,
|
||||||
COLOR_BORDER_DARK);
|
COLOR_BORDER_DARK);
|
||||||
fit_text_with_ellipsis(is_selected ? fonts->medium : fonts->small,
|
fit_text_with_ellipsis(is_selected ? fonts->medium : fonts->small,
|
||||||
channel->program_title,
|
program->program_title,
|
||||||
title_rect.w,
|
title_rect.w,
|
||||||
title,
|
title,
|
||||||
sizeof(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) {
|
for (int i = 0; i < channels->count; ++i) {
|
||||||
const Channel *channel = &channels->items[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);
|
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 ||
|
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].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].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, channel->program_title, COLOR_TEXT_LIGHT) != 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, channel->program_title, COLOR_TEXT_DARK) != 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);
|
ui_cache_destroy(cache);
|
||||||
return -1;
|
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) {
|
if (text_texture_init(&cache->channels[i].detail_small_dark, renderer, fonts->small, buffer, COLOR_TEXT_DARK) != 0) {
|
||||||
ui_cache_destroy(cache);
|
ui_cache_destroy(cache);
|
||||||
return -1;
|
return -1;
|
||||||
|
|
|
||||||
BIN
src/ui.o
BIN
src/ui.o
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue