Compare commits
10 commits
7bbcf2e5f3
...
c9da0bbefb
| Author | SHA1 | Date | |
|---|---|---|---|
| c9da0bbefb | |||
| 0a7e9dcdfb | |||
| 177933878b | |||
| fe5fccbd02 | |||
| f64429b7b4 | |||
| 126469df76 | |||
| ee8bf2dd0b | |||
| 56a802b502 | |||
| 538c676828 | |||
| 1217903703 |
18 changed files with 475 additions and 101 deletions
4
Makefile
4
Makefile
|
|
@ -1,6 +1,7 @@
|
||||||
CC ?= cc
|
CC ?= cc
|
||||||
CSTD ?= -std=c11
|
CSTD ?= -std=c11
|
||||||
CFLAGS ?= -O2 -Wall -Wextra -Wpedantic $(CSTD)
|
CFLAGS ?= -O2 -Wall -Wextra -Wpedantic $(CSTD)
|
||||||
|
CFLAGS += -MMD -MP
|
||||||
|
|
||||||
SDL_CFLAGS := $(shell pkg-config --cflags SDL2 SDL2_ttf SDL2_image 2>/dev/null)
|
SDL_CFLAGS := $(shell pkg-config --cflags SDL2 SDL2_ttf SDL2_image 2>/dev/null)
|
||||||
SDL_LIBS := $(shell pkg-config --libs SDL2 SDL2_ttf SDL2_image 2>/dev/null)
|
SDL_LIBS := $(shell pkg-config --libs SDL2 SDL2_ttf SDL2_image 2>/dev/null)
|
||||||
|
|
@ -19,6 +20,7 @@ endif
|
||||||
|
|
||||||
SRC := $(wildcard src/*.c)
|
SRC := $(wildcard src/*.c)
|
||||||
OBJ := $(SRC:.c=.o)
|
OBJ := $(SRC:.c=.o)
|
||||||
|
DEP := $(OBJ:.o=.d)
|
||||||
BIN := passport-c-media-player
|
BIN := passport-c-media-player
|
||||||
|
|
||||||
CPPFLAGS += -I./src $(SDL_CFLAGS) $(FFMPEG_CFLAGS)
|
CPPFLAGS += -I./src $(SDL_CFLAGS) $(FFMPEG_CFLAGS)
|
||||||
|
|
@ -31,6 +33,8 @@ all: $(BIN)
|
||||||
$(BIN): $(OBJ)
|
$(BIN): $(OBJ)
|
||||||
$(CC) $(OBJ) -o $@ $(LDLIBS)
|
$(CC) $(OBJ) -o $@ $(LDLIBS)
|
||||||
|
|
||||||
|
-include $(DEP)
|
||||||
|
|
||||||
run: $(BIN)
|
run: $(BIN)
|
||||||
./$(BIN)
|
./$(BIN)
|
||||||
|
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Passport-C Media Player
|
# FreePassport-C Media Player
|
||||||
|
|
||||||
Passport-C Media Player is a low-dependency SDL2 + FFmpeg prototype that recreates the 2004-era Passport DCT cable guide with live-seeking channels.
|
FreePassport-C Media Player is a low-dependency SDL2 + FFmpeg application that is based on the look of interactive programming guide software used on older Cisco/Scientific Atlanta Explorer cable boxes, it allows you to organize your media into a virtual cable guide with live-seeking channels.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
@ -21,6 +21,7 @@ Passport-C Media Player is a low-dependency SDL2 + FFmpeg prototype that recreat
|
||||||
|
|
||||||
- `Up` / `Down` - surf channels
|
- `Up` / `Down` - surf channels
|
||||||
- `Tab` - toggle guide
|
- `Tab` - toggle guide
|
||||||
|
- `i` - show current channel info
|
||||||
- `Esc` - exit guide or quit the app
|
- `Esc` - exit guide or quit the app
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
@ -48,10 +49,17 @@ To create a portable AppImage that can run on any Linux distribution:
|
||||||
make appimage
|
make appimage
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates `Passport-C-Media-Player-0.1-x86_64.AppImage`, a self-contained executable that includes all dependencies. Users can run it directly:
|
This creates a self-contained executable that includes all dependencies. The script automatically detects your architecture and produces the appropriate AppImage:
|
||||||
|
|
||||||
|
- **x86_64**: `Passport-C-Media-Player-0.1-x86_64.AppImage`
|
||||||
|
- **ARM64/aarch64**: `Passport-C-Media-Player-0.1-aarch64.AppImage`
|
||||||
|
|
||||||
|
Users can run it directly:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./Passport-C-Media-Player-0.1-x86_64.AppImage
|
./Passport-C-Media-Player-0.1-x86_64.AppImage
|
||||||
|
# or on ARM devices:
|
||||||
|
./Passport-C-Media-Player-0.1-aarch64.AppImage
|
||||||
```
|
```
|
||||||
|
|
||||||
For a fully portable setup, create a `media/` directory in the same folder as the AppImage and add your channel videos there. The AppImage will automatically detect and load channels from this directory.
|
For a fully portable setup, create a `media/` directory in the same folder as the AppImage and add your channel videos there. The AppImage will automatically detect and load channels from this directory.
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
APP_NAME="Passport-C-Media-Player"
|
APP_NAME="FreePassport-C-Media-Player"
|
||||||
APP_VERSION="0.1"
|
APP_VERSION="0.1"
|
||||||
OUTPUT_NAME="${APP_NAME}-${APP_VERSION}-x86_64.AppImage"
|
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
|
|
@ -12,9 +11,29 @@ GREEN='\033[0;32m'
|
||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Detect architecture
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
case $ARCH in
|
||||||
|
x86_64)
|
||||||
|
LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
|
||||||
|
LINUXDEPLOY_BIN="linuxdeploy-x86_64.AppImage"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-aarch64.AppImage"
|
||||||
|
LINUXDEPLOY_BIN="linuxdeploy-aarch64.AppImage"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Error: Unsupported architecture: $ARCH${NC}"
|
||||||
|
echo -e "${RED}Supported architectures: x86_64, aarch64${NC}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
OUTPUT_NAME="${APP_NAME}-${APP_VERSION}-${ARCH}.AppImage"
|
||||||
|
|
||||||
rm -rf AppDir/
|
rm -rf AppDir/
|
||||||
|
|
||||||
echo -e "${GREEN}Building ${APP_NAME} AppImage...${NC}"
|
echo -e "${GREEN}Building ${APP_NAME} AppImage for ${ARCH}...${NC}"
|
||||||
|
|
||||||
# Check if binary exists
|
# Check if binary exists
|
||||||
if [ ! -f "passport-c-media-player" ]; then
|
if [ ! -f "passport-c-media-player" ]; then
|
||||||
|
|
@ -33,10 +52,18 @@ mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps
|
||||||
cp passport-c-media-player AppDir/usr/bin/
|
cp passport-c-media-player AppDir/usr/bin/
|
||||||
chmod +x AppDir/usr/bin/passport-c-media-player
|
chmod +x AppDir/usr/bin/passport-c-media-player
|
||||||
|
|
||||||
|
# Copy bundled resources (font and logo for AppImage)
|
||||||
|
if [ -f "BigBlueTermPlusNerdFontMono-Regular.ttf" ]; then
|
||||||
|
cp BigBlueTermPlusNerdFontMono-Regular.ttf AppDir/usr/bin/
|
||||||
|
fi
|
||||||
|
if [ -f "logo.png" ]; then
|
||||||
|
cp logo.png AppDir/usr/bin/
|
||||||
|
fi
|
||||||
|
|
||||||
# Create desktop file directly in script (avoids copy conflicts)
|
# Create desktop file directly in script (avoids copy conflicts)
|
||||||
cat > AppDir/passport-c-media-player.desktop << 'EOF'
|
cat > AppDir/passport-c-media-player.desktop << 'EOF'
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=Passport-C Media Player
|
Name=FreePassport-C Media Player
|
||||||
Comment=Turn your media library into an early 2000s cable box TV guide
|
Comment=Turn your media library into an early 2000s cable box TV guide
|
||||||
Exec=passport-c-media-player
|
Exec=passport-c-media-player
|
||||||
Icon=passport-c-media-player
|
Icon=passport-c-media-player
|
||||||
|
|
@ -63,15 +90,15 @@ ln -sf usr/bin/passport-c-media-player AppRun
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
# Download linuxdeploy if not present
|
# Download linuxdeploy if not present
|
||||||
if [ ! -f "linuxdeploy-x86_64.AppImage" ]; then
|
if [ ! -f "${LINUXDEPLOY_BIN}" ]; then
|
||||||
echo -e "${GREEN}Downloading linuxdeploy...${NC}"
|
echo -e "${GREEN}Downloading linuxdeploy for ${ARCH}...${NC}"
|
||||||
wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
wget -q "${LINUXDEPLOY_URL}" -O "${LINUXDEPLOY_BIN}"
|
||||||
chmod +x linuxdeploy-x86_64.AppImage
|
chmod +x "${LINUXDEPLOY_BIN}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build AppImage
|
# Build AppImage
|
||||||
echo -e "${GREEN}Creating AppImage...${NC}"
|
echo -e "${GREEN}Creating AppImage for ${ARCH}...${NC}"
|
||||||
./linuxdeploy-x86_64.AppImage \
|
./"${LINUXDEPLOY_BIN}" \
|
||||||
--appdir AppDir \
|
--appdir AppDir \
|
||||||
--output appimage \
|
--output appimage \
|
||||||
-d AppDir/usr/share/applications/passport-c-media-player.desktop \
|
-d AppDir/usr/share/applications/passport-c-media-player.desktop \
|
||||||
|
|
@ -79,7 +106,7 @@ echo -e "${GREEN}Creating AppImage...${NC}"
|
||||||
|
|
||||||
# Rename output file
|
# Rename output file
|
||||||
# Use wildcard to find whatever AppImage was created (handles underscore/dash variations)
|
# Use wildcard to find whatever AppImage was created (handles underscore/dash variations)
|
||||||
for file in Passport*.AppImage; do
|
for file in FreePassport-C_Media_Player-*.AppImage; do
|
||||||
if [ -f "$file" ]; then
|
if [ -f "$file" ]; then
|
||||||
mv "$file" "${OUTPUT_NAME}"
|
mv "$file" "${OUTPUT_NAME}"
|
||||||
break
|
break
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 94 KiB |
32
src/app.c
32
src/app.c
|
|
@ -15,6 +15,7 @@
|
||||||
#define CHANNEL_BANNER_DURATION_MS 5000
|
#define CHANNEL_BANNER_DURATION_MS 5000
|
||||||
#define CHANNEL_SWITCH_LOCK_DURATION_MS 7000
|
#define CHANNEL_SWITCH_LOCK_DURATION_MS 7000
|
||||||
#define NUMERIC_INPUT_TIMEOUT_MS 1000
|
#define NUMERIC_INPUT_TIMEOUT_MS 1000
|
||||||
|
#define GUIDE_PREVIEW_FRAME_INTERVAL_MS 83
|
||||||
|
|
||||||
static void configure_runtime_environment(void) {
|
static void configure_runtime_environment(void) {
|
||||||
char runtime_dir[64];
|
char runtime_dir[64];
|
||||||
|
|
@ -66,6 +67,9 @@ static void destroy_video_texture(App *app) {
|
||||||
app->texture_width = 0;
|
app->texture_width = 0;
|
||||||
app->texture_height = 0;
|
app->texture_height = 0;
|
||||||
}
|
}
|
||||||
|
if (app) {
|
||||||
|
app->last_guide_preview_update = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void begin_startup_handoff(App *app) {
|
static void begin_startup_handoff(App *app) {
|
||||||
|
|
@ -82,8 +86,25 @@ static int update_video_texture(App *app) {
|
||||||
double sync_clock;
|
double sync_clock;
|
||||||
double lead_tolerance;
|
double lead_tolerance;
|
||||||
Uint64 now_ticks;
|
Uint64 now_ticks;
|
||||||
|
Uint32 update_ticks;
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
now_ticks = SDL_GetTicks64();
|
now_ticks = SDL_GetTicks64();
|
||||||
|
update_ticks = (Uint32) now_ticks;
|
||||||
|
|
||||||
|
if (app->mode == MODE_GUIDE &&
|
||||||
|
app->video_texture &&
|
||||||
|
update_ticks - app->last_guide_preview_update < GUIDE_PREVIEW_FRAME_INTERVAL_MS) {
|
||||||
|
if (app->startup_handoff_active && app->startup_handoff_until != 0 && SDL_GetTicks() >= app->startup_handoff_until) {
|
||||||
|
app->startup_handoff_active = 0;
|
||||||
|
app->startup_handoff_until = 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
sync_clock = player_get_sync_clock(&app->player, now_ticks);
|
sync_clock = player_get_sync_clock(&app->player, now_ticks);
|
||||||
lead_tolerance = app->startup_handoff_active ? 0.240 : 0.060;
|
lead_tolerance = app->startup_handoff_active ? 0.240 : 0.060;
|
||||||
|
|
||||||
|
|
@ -119,6 +140,10 @@ static int update_video_texture(App *app) {
|
||||||
frame.pitch_v);
|
frame.pitch_v);
|
||||||
frame_data_free(&frame);
|
frame_data_free(&frame);
|
||||||
|
|
||||||
|
if (app->mode == MODE_GUIDE) {
|
||||||
|
app->last_guide_preview_update = update_ticks;
|
||||||
|
}
|
||||||
|
|
||||||
if (app->startup_handoff_active && app->startup_handoff_until != 0 && SDL_GetTicks() >= app->startup_handoff_until) {
|
if (app->startup_handoff_active && app->startup_handoff_until != 0 && SDL_GetTicks() >= app->startup_handoff_until) {
|
||||||
app->startup_handoff_active = 0;
|
app->startup_handoff_active = 0;
|
||||||
app->startup_handoff_until = 0;
|
app->startup_handoff_until = 0;
|
||||||
|
|
@ -374,6 +399,11 @@ static void handle_event(App *app, const SDL_Event *event) {
|
||||||
tune_relative(app, -1);
|
tune_relative(app, -1);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case SDLK_i:
|
||||||
|
if (app->mode == MODE_FULLSCREEN) {
|
||||||
|
app->channel_banner_until = SDL_GetTicks() + CHANNEL_BANNER_DURATION_MS;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case SDLK_LEFT:
|
case SDLK_LEFT:
|
||||||
browse_guide_time(app, -GUIDE_BROWSE_STEP_MINUTES);
|
browse_guide_time(app, -GUIDE_BROWSE_STEP_MINUTES);
|
||||||
break;
|
break;
|
||||||
|
|
@ -423,7 +453,7 @@ int app_init(App *app) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
app->window = SDL_CreateWindow("Passport-C Media Player",
|
app->window = SDL_CreateWindow("FreePassport-C Media Player",
|
||||||
SDL_WINDOWPOS_CENTERED,
|
SDL_WINDOWPOS_CENTERED,
|
||||||
SDL_WINDOWPOS_CENTERED,
|
SDL_WINDOWPOS_CENTERED,
|
||||||
WINDOW_WIDTH,
|
WINDOW_WIDTH,
|
||||||
|
|
|
||||||
8
src/app.d
Normal file
8
src/app.d
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
src/app.o: src/app.c src/app.h src/channel.h src/player.h \
|
||||||
|
src/frame_queue.h src/theme.h src/ui.h
|
||||||
|
src/app.h:
|
||||||
|
src/channel.h:
|
||||||
|
src/player.h:
|
||||||
|
src/frame_queue.h:
|
||||||
|
src/theme.h:
|
||||||
|
src/ui.h:
|
||||||
|
|
@ -24,6 +24,7 @@ typedef struct App {
|
||||||
int startup_handoff_active;
|
int startup_handoff_active;
|
||||||
int last_blackout_state;
|
int last_blackout_state;
|
||||||
Uint32 startup_handoff_until;
|
Uint32 startup_handoff_until;
|
||||||
|
Uint32 last_guide_preview_update;
|
||||||
Uint32 channel_banner_until;
|
Uint32 channel_banner_until;
|
||||||
Uint32 channel_switch_lock_until;
|
Uint32 channel_switch_lock_until;
|
||||||
time_t app_start_time;
|
time_t app_start_time;
|
||||||
|
|
|
||||||
8
src/channel.d
Normal file
8
src/channel.d
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
src/channel.o: src/channel.c src/channel.h \
|
||||||
|
/usr/include/i386-linux-gnu/sys/stat.h \
|
||||||
|
/usr/include/i386-linux-gnu/bits/stat.h \
|
||||||
|
/usr/include/i386-linux-gnu/bits/struct_stat.h
|
||||||
|
src/channel.h:
|
||||||
|
/usr/include/i386-linux-gnu/sys/stat.h:
|
||||||
|
/usr/include/i386-linux-gnu/bits/stat.h:
|
||||||
|
/usr/include/i386-linux-gnu/bits/struct_stat.h:
|
||||||
|
|
@ -9,10 +9,114 @@ void frame_data_free(FrameData *frame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
av_freep(&frame->buffer);
|
if (frame->pool && frame->buffer) {
|
||||||
|
video_buffer_pool_release(frame->pool, frame->buffer);
|
||||||
|
}
|
||||||
memset(frame, 0, sizeof(*frame));
|
memset(frame, 0, sizeof(*frame));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int frame_queue_is_full(FrameQueue *queue) {
|
||||||
|
int full;
|
||||||
|
if (!queue || !queue->mutex) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
SDL_LockMutex(queue->mutex);
|
||||||
|
full = (queue->count >= FRAME_QUEUE_CAPACITY);
|
||||||
|
SDL_UnlockMutex(queue->mutex);
|
||||||
|
return full;
|
||||||
|
}
|
||||||
|
|
||||||
|
int video_buffer_pool_init(VideoBufferPool *pool) {
|
||||||
|
int i;
|
||||||
|
int yuv420p_size;
|
||||||
|
|
||||||
|
if (!pool) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(pool, 0, sizeof(*pool));
|
||||||
|
pool->mutex = SDL_CreateMutex();
|
||||||
|
if (!pool->mutex) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
yuv420p_size = TARGET_WIDTH * TARGET_HEIGHT * 3 / 2;
|
||||||
|
pool->buffer_size = yuv420p_size;
|
||||||
|
|
||||||
|
for (i = 0; i < VIDEO_BUFFER_POOL_SIZE; i++) {
|
||||||
|
pool->buffers[i] = (unsigned char *)av_malloc(yuv420p_size + 32);
|
||||||
|
if (!pool->buffers[i]) {
|
||||||
|
video_buffer_pool_destroy(pool);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
pool->available[i] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void video_buffer_pool_destroy(VideoBufferPool *pool) {
|
||||||
|
int i;
|
||||||
|
if (!pool) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pool->mutex) {
|
||||||
|
SDL_LockMutex(pool->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < VIDEO_BUFFER_POOL_SIZE; i++) {
|
||||||
|
if (pool->buffers[i]) {
|
||||||
|
av_freep(&pool->buffers[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pool->mutex) {
|
||||||
|
SDL_UnlockMutex(pool->mutex);
|
||||||
|
SDL_DestroyMutex(pool->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(pool, 0, sizeof(*pool));
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char *video_buffer_pool_acquire(VideoBufferPool *pool) {
|
||||||
|
unsigned char *buffer = NULL;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (!pool || !pool->mutex) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LockMutex(pool->mutex);
|
||||||
|
for (i = 0; i < VIDEO_BUFFER_POOL_SIZE; i++) {
|
||||||
|
if (pool->available[i]) {
|
||||||
|
pool->available[i] = 0;
|
||||||
|
buffer = pool->buffers[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SDL_UnlockMutex(pool->mutex);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void video_buffer_pool_release(VideoBufferPool *pool, unsigned char *buffer) {
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (!pool || !pool->mutex || !buffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LockMutex(pool->mutex);
|
||||||
|
for (i = 0; i < VIDEO_BUFFER_POOL_SIZE; i++) {
|
||||||
|
if (pool->buffers[i] == buffer) {
|
||||||
|
pool->available[i] = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SDL_UnlockMutex(pool->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
int frame_queue_init(FrameQueue *queue) {
|
int frame_queue_init(FrameQueue *queue) {
|
||||||
if (!queue) {
|
if (!queue) {
|
||||||
return -1;
|
return -1;
|
||||||
|
|
|
||||||
2
src/frame_queue.d
Normal file
2
src/frame_queue.d
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
src/frame_queue.o: src/frame_queue.c src/frame_queue.h
|
||||||
|
src/frame_queue.h:
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
#ifndef FRAME_QUEUE_H
|
#ifndef FRAME_QUEUE_H
|
||||||
#define FRAME_QUEUE_H
|
#define FRAME_QUEUE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
#define FRAME_QUEUE_CAPACITY 12
|
#define FRAME_QUEUE_CAPACITY 6
|
||||||
|
#define TARGET_WIDTH 640
|
||||||
|
#define TARGET_HEIGHT 360
|
||||||
|
#define VIDEO_BUFFER_POOL_SIZE 16
|
||||||
|
|
||||||
typedef struct FrameData {
|
typedef struct FrameData {
|
||||||
unsigned char *buffer;
|
unsigned char *buffer;
|
||||||
|
struct VideoBufferPool *pool;
|
||||||
unsigned char *plane_y;
|
unsigned char *plane_y;
|
||||||
unsigned char *plane_u;
|
unsigned char *plane_u;
|
||||||
unsigned char *plane_v;
|
unsigned char *plane_v;
|
||||||
|
|
@ -26,6 +31,13 @@ typedef struct FrameQueue {
|
||||||
SDL_cond *cond;
|
SDL_cond *cond;
|
||||||
} FrameQueue;
|
} FrameQueue;
|
||||||
|
|
||||||
|
typedef struct VideoBufferPool {
|
||||||
|
unsigned char *buffers[VIDEO_BUFFER_POOL_SIZE];
|
||||||
|
int buffer_size;
|
||||||
|
int available[VIDEO_BUFFER_POOL_SIZE];
|
||||||
|
SDL_mutex *mutex;
|
||||||
|
} VideoBufferPool;
|
||||||
|
|
||||||
int frame_queue_init(FrameQueue *queue);
|
int frame_queue_init(FrameQueue *queue);
|
||||||
void frame_queue_destroy(FrameQueue *queue);
|
void frame_queue_destroy(FrameQueue *queue);
|
||||||
void frame_queue_clear(FrameQueue *queue);
|
void frame_queue_clear(FrameQueue *queue);
|
||||||
|
|
@ -34,5 +46,11 @@ int frame_queue_pop_latest(FrameQueue *queue, FrameData *out);
|
||||||
int frame_queue_peek_first(FrameQueue *queue, FrameData *out);
|
int frame_queue_peek_first(FrameQueue *queue, FrameData *out);
|
||||||
int frame_queue_pop_first(FrameQueue *queue, FrameData *out);
|
int frame_queue_pop_first(FrameQueue *queue, FrameData *out);
|
||||||
void frame_data_free(FrameData *frame);
|
void frame_data_free(FrameData *frame);
|
||||||
|
int frame_queue_is_full(FrameQueue *queue);
|
||||||
|
|
||||||
|
int video_buffer_pool_init(VideoBufferPool *pool);
|
||||||
|
void video_buffer_pool_destroy(VideoBufferPool *pool);
|
||||||
|
unsigned char *video_buffer_pool_acquire(VideoBufferPool *pool);
|
||||||
|
void video_buffer_pool_release(VideoBufferPool *pool, unsigned char *buffer);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
8
src/main.d
Normal file
8
src/main.d
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
src/main.o: src/main.c src/app.h src/channel.h src/player.h \
|
||||||
|
src/frame_queue.h src/theme.h src/ui.h
|
||||||
|
src/app.h:
|
||||||
|
src/channel.h:
|
||||||
|
src/player.h:
|
||||||
|
src/frame_queue.h:
|
||||||
|
src/theme.h:
|
||||||
|
src/ui.h:
|
||||||
125
src/player.c
125
src/player.c
|
|
@ -15,6 +15,8 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#define AUDIO_BUFFER_SAMPLES 48000
|
||||||
|
|
||||||
typedef struct DecoderThreadArgs {
|
typedef struct DecoderThreadArgs {
|
||||||
Player *player;
|
Player *player;
|
||||||
const Channel *channel;
|
const Channel *channel;
|
||||||
|
|
@ -186,7 +188,7 @@ static int queue_audio_frame(Player *player,
|
||||||
AVFrame *audio_frame,
|
AVFrame *audio_frame,
|
||||||
double pts_base_offset,
|
double pts_base_offset,
|
||||||
int *queued_any_audio) {
|
int *queued_any_audio) {
|
||||||
uint8_t **converted_data = NULL;
|
uint8_t *converted_data[1] = {NULL};
|
||||||
int out_rate;
|
int out_rate;
|
||||||
int out_channels;
|
int out_channels;
|
||||||
enum AVSampleFormat out_format;
|
enum AVSampleFormat out_format;
|
||||||
|
|
@ -196,10 +198,10 @@ static int queue_audio_frame(Player *player,
|
||||||
int sample_count;
|
int sample_count;
|
||||||
int buffer_size;
|
int buffer_size;
|
||||||
int queued_limit;
|
int queued_limit;
|
||||||
int line_size = 0;
|
|
||||||
int result;
|
int result;
|
||||||
double audio_pts;
|
double audio_pts;
|
||||||
double frame_duration;
|
double frame_duration;
|
||||||
|
int wait_ms = 2;
|
||||||
|
|
||||||
if (!player || !audio_codec_context || !audio_frame) {
|
if (!player || !audio_codec_context || !audio_frame) {
|
||||||
return -1;
|
return -1;
|
||||||
|
|
@ -236,16 +238,20 @@ static int queue_audio_frame(Player *player,
|
||||||
audio_codec_context->sample_rate,
|
audio_codec_context->sample_rate,
|
||||||
AV_ROUND_UP);
|
AV_ROUND_UP);
|
||||||
|
|
||||||
if (av_samples_alloc_array_and_samples(&converted_data,
|
if (max_samples > player->audio_convert_buffer_samples) {
|
||||||
&line_size,
|
int new_size = max_samples * out_channels * sizeof(int16_t);
|
||||||
out_channels,
|
uint8_t *new_buffer = (uint8_t *)av_realloc(player->audio_convert_buffer, new_size);
|
||||||
max_samples,
|
if (!new_buffer) {
|
||||||
out_format,
|
|
||||||
0) < 0) {
|
|
||||||
av_channel_layout_uninit(&out_layout);
|
av_channel_layout_uninit(&out_layout);
|
||||||
player_set_error(player, "Unable to allocate audio buffer");
|
player_set_error(player, "Unable to resize audio buffer");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
player->audio_convert_buffer = new_buffer;
|
||||||
|
player->audio_convert_buffer_samples = max_samples;
|
||||||
|
player->audio_convert_buffer_size = new_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
converted_data[0] = player->audio_convert_buffer;
|
||||||
|
|
||||||
sample_count = swr_convert(*swr_context,
|
sample_count = swr_convert(*swr_context,
|
||||||
converted_data,
|
converted_data,
|
||||||
|
|
@ -253,17 +259,13 @@ static int queue_audio_frame(Player *player,
|
||||||
(const uint8_t * const *) audio_frame->extended_data,
|
(const uint8_t * const *) audio_frame->extended_data,
|
||||||
audio_frame->nb_samples);
|
audio_frame->nb_samples);
|
||||||
if (sample_count < 0) {
|
if (sample_count < 0) {
|
||||||
av_freep(&converted_data[0]);
|
|
||||||
av_freep(&converted_data);
|
|
||||||
av_channel_layout_uninit(&out_layout);
|
av_channel_layout_uninit(&out_layout);
|
||||||
player_set_error(player, "Audio resample failed");
|
player_set_error(player, "Audio resample failed");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer_size = av_samples_get_buffer_size(&line_size, out_channels, sample_count, out_format, 1);
|
buffer_size = sample_count * out_channels * sizeof(int16_t);
|
||||||
if (buffer_size <= 0) {
|
if (buffer_size <= 0) {
|
||||||
av_freep(&converted_data[0]);
|
|
||||||
av_freep(&converted_data);
|
|
||||||
av_channel_layout_uninit(&out_layout);
|
av_channel_layout_uninit(&out_layout);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -275,12 +277,11 @@ static int queue_audio_frame(Player *player,
|
||||||
|
|
||||||
queued_limit = (int) (player_audio_bytes_per_second(player) * 0.18);
|
queued_limit = (int) (player_audio_bytes_per_second(player) * 0.18);
|
||||||
while (!should_stop(player) && (int) SDL_GetQueuedAudioSize(player->audio_device) > queued_limit) {
|
while (!should_stop(player) && (int) SDL_GetQueuedAudioSize(player->audio_device) > queued_limit) {
|
||||||
SDL_Delay(2);
|
SDL_Delay(wait_ms);
|
||||||
|
wait_ms = (wait_ms < 10) ? wait_ms + 1 : 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
result = SDL_QueueAudio(player->audio_device, converted_data[0], (Uint32) buffer_size);
|
result = SDL_QueueAudio(player->audio_device, player->audio_convert_buffer, (Uint32) buffer_size);
|
||||||
av_freep(&converted_data[0]);
|
|
||||||
av_freep(&converted_data);
|
|
||||||
|
|
||||||
if (result != 0) {
|
if (result != 0) {
|
||||||
av_channel_layout_uninit(&out_layout);
|
av_channel_layout_uninit(&out_layout);
|
||||||
|
|
@ -403,8 +404,8 @@ static int decode_context_open(DecodeContext *ctx, const ProgramEntry *program,
|
||||||
ctx->sws_context = sws_getContext(ctx->video_codec_context->width,
|
ctx->sws_context = sws_getContext(ctx->video_codec_context->width,
|
||||||
ctx->video_codec_context->height,
|
ctx->video_codec_context->height,
|
||||||
ctx->video_codec_context->pix_fmt,
|
ctx->video_codec_context->pix_fmt,
|
||||||
ctx->video_codec_context->width,
|
TARGET_WIDTH,
|
||||||
ctx->video_codec_context->height,
|
TARGET_HEIGHT,
|
||||||
AV_PIX_FMT_YUV420P,
|
AV_PIX_FMT_YUV420P,
|
||||||
SWS_BILINEAR,
|
SWS_BILINEAR,
|
||||||
NULL,
|
NULL,
|
||||||
|
|
@ -612,23 +613,40 @@ static int decode_thread_main(void *userdata) {
|
||||||
FrameData frame = {0};
|
FrameData frame = {0};
|
||||||
uint8_t *dest_data[4] = {0};
|
uint8_t *dest_data[4] = {0};
|
||||||
int dest_linesize[4] = {0};
|
int dest_linesize[4] = {0};
|
||||||
int image_size;
|
int needs_conversion = 1;
|
||||||
|
|
||||||
frame.width = ctx.video_codec_context->width;
|
while (!should_stop(player) && frame_queue_is_full(&player->frame_queue)) {
|
||||||
frame.height = ctx.video_codec_context->height;
|
SDL_Delay(5);
|
||||||
|
}
|
||||||
|
if (should_stop(player)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.width = TARGET_WIDTH;
|
||||||
|
frame.height = TARGET_HEIGHT;
|
||||||
frame.pts_seconds = frame_seconds;
|
frame.pts_seconds = frame_seconds;
|
||||||
image_size = av_image_alloc(dest_data, dest_linesize, frame.width, frame.height, AV_PIX_FMT_YUV420P, 1);
|
|
||||||
if (image_size < 0) {
|
frame.buffer = video_buffer_pool_acquire(&player->video_buffer_pool);
|
||||||
player_set_error(player, "Unable to allocate frame buffer");
|
if (!frame.buffer) {
|
||||||
|
player_set_error(player, "Unable to acquire frame buffer from pool");
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
frame.buffer = dest_data[0];
|
|
||||||
frame.plane_y = dest_data[0];
|
frame.pool = &player->video_buffer_pool;
|
||||||
frame.plane_u = dest_data[1];
|
dest_data[0] = frame.buffer;
|
||||||
frame.plane_v = dest_data[2];
|
dest_data[1] = frame.buffer + (TARGET_WIDTH * TARGET_HEIGHT);
|
||||||
frame.pitch_y = dest_linesize[0];
|
dest_data[2] = frame.buffer + (TARGET_WIDTH * TARGET_HEIGHT * 5 / 4);
|
||||||
frame.pitch_u = dest_linesize[1];
|
dest_linesize[0] = TARGET_WIDTH;
|
||||||
frame.pitch_v = dest_linesize[2];
|
dest_linesize[1] = TARGET_WIDTH / 2;
|
||||||
|
dest_linesize[2] = TARGET_WIDTH / 2;
|
||||||
|
|
||||||
|
if (ctx.video_codec_context->width == TARGET_WIDTH &&
|
||||||
|
ctx.video_codec_context->height == TARGET_HEIGHT &&
|
||||||
|
ctx.video_codec_context->pix_fmt == AV_PIX_FMT_YUV420P) {
|
||||||
|
needs_conversion = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needs_conversion) {
|
||||||
sws_scale(ctx.sws_context,
|
sws_scale(ctx.sws_context,
|
||||||
(const uint8_t *const *) ctx.decoded_frame->data,
|
(const uint8_t *const *) ctx.decoded_frame->data,
|
||||||
ctx.decoded_frame->linesize,
|
ctx.decoded_frame->linesize,
|
||||||
|
|
@ -636,6 +654,21 @@ static int decode_thread_main(void *userdata) {
|
||||||
ctx.video_codec_context->height,
|
ctx.video_codec_context->height,
|
||||||
dest_data,
|
dest_data,
|
||||||
dest_linesize);
|
dest_linesize);
|
||||||
|
} else {
|
||||||
|
av_image_copy(dest_data, dest_linesize,
|
||||||
|
(const uint8_t *const *) ctx.decoded_frame->data,
|
||||||
|
ctx.decoded_frame->linesize,
|
||||||
|
AV_PIX_FMT_YUV420P,
|
||||||
|
TARGET_WIDTH, TARGET_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.plane_y = dest_data[0];
|
||||||
|
frame.plane_u = dest_data[1];
|
||||||
|
frame.plane_v = dest_data[2];
|
||||||
|
frame.pitch_y = dest_linesize[0];
|
||||||
|
frame.pitch_u = dest_linesize[1];
|
||||||
|
frame.pitch_v = dest_linesize[2];
|
||||||
|
|
||||||
frame_queue_push(&player->frame_queue, &frame);
|
frame_queue_push(&player->frame_queue, &frame);
|
||||||
queued_first_video = 1;
|
queued_first_video = 1;
|
||||||
MAYBE_MARK_PREROLL_READY();
|
MAYBE_MARK_PREROLL_READY();
|
||||||
|
|
@ -696,6 +729,28 @@ int player_init(Player *player, const ChannelList *channels, Uint64 app_start_ti
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (video_buffer_pool_init(&player->video_buffer_pool) != 0) {
|
||||||
|
frame_queue_destroy(&player->frame_queue);
|
||||||
|
SDL_DestroyMutex(player->audio_mutex);
|
||||||
|
SDL_DestroyMutex(player->clock_mutex);
|
||||||
|
SDL_DestroyMutex(player->error_mutex);
|
||||||
|
memset(player, 0, sizeof(*player));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
player->audio_convert_buffer_samples = AUDIO_BUFFER_SAMPLES;
|
||||||
|
player->audio_convert_buffer_size = AUDIO_BUFFER_SAMPLES * 2 * sizeof(int16_t);
|
||||||
|
player->audio_convert_buffer = (uint8_t *)av_malloc(player->audio_convert_buffer_size);
|
||||||
|
if (!player->audio_convert_buffer) {
|
||||||
|
video_buffer_pool_destroy(&player->video_buffer_pool);
|
||||||
|
frame_queue_destroy(&player->frame_queue);
|
||||||
|
SDL_DestroyMutex(player->audio_mutex);
|
||||||
|
SDL_DestroyMutex(player->clock_mutex);
|
||||||
|
SDL_DestroyMutex(player->error_mutex);
|
||||||
|
memset(player, 0, sizeof(*player));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -706,6 +761,10 @@ void player_destroy(Player *player) {
|
||||||
|
|
||||||
player_stop_thread(player);
|
player_stop_thread(player);
|
||||||
player_close_audio(player);
|
player_close_audio(player);
|
||||||
|
video_buffer_pool_destroy(&player->video_buffer_pool);
|
||||||
|
if (player->audio_convert_buffer) {
|
||||||
|
av_freep(&player->audio_convert_buffer);
|
||||||
|
}
|
||||||
frame_queue_destroy(&player->frame_queue);
|
frame_queue_destroy(&player->frame_queue);
|
||||||
if (player->audio_mutex) {
|
if (player->audio_mutex) {
|
||||||
SDL_DestroyMutex(player->audio_mutex);
|
SDL_DestroyMutex(player->audio_mutex);
|
||||||
|
|
|
||||||
4
src/player.d
Normal file
4
src/player.d
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
src/player.o: src/player.c src/player.h src/channel.h src/frame_queue.h
|
||||||
|
src/player.h:
|
||||||
|
src/channel.h:
|
||||||
|
src/frame_queue.h:
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#ifndef PLAYER_H
|
#ifndef PLAYER_H
|
||||||
#define PLAYER_H
|
#define PLAYER_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
|
|
@ -9,6 +10,7 @@
|
||||||
|
|
||||||
typedef struct Player {
|
typedef struct Player {
|
||||||
FrameQueue frame_queue;
|
FrameQueue frame_queue;
|
||||||
|
VideoBufferPool video_buffer_pool;
|
||||||
SDL_Thread *thread;
|
SDL_Thread *thread;
|
||||||
SDL_atomic_t stop_requested;
|
SDL_atomic_t stop_requested;
|
||||||
const ChannelList *channels;
|
const ChannelList *channels;
|
||||||
|
|
@ -27,6 +29,9 @@ typedef struct Player {
|
||||||
char audio_driver[32];
|
char audio_driver[32];
|
||||||
SDL_mutex *error_mutex;
|
SDL_mutex *error_mutex;
|
||||||
char last_error[256];
|
char last_error[256];
|
||||||
|
uint8_t *audio_convert_buffer;
|
||||||
|
int audio_convert_buffer_size;
|
||||||
|
int audio_convert_buffer_samples;
|
||||||
} Player;
|
} Player;
|
||||||
|
|
||||||
int player_init(Player *player, const ChannelList *channels, Uint64 app_start_ticks);
|
int player_init(Player *player, const ChannelList *channels, Uint64 app_start_ticks);
|
||||||
|
|
|
||||||
13
src/theme.h
13
src/theme.h
|
|
@ -110,8 +110,9 @@ static const SDL_Color COLOR_PILL_SHADOW = {0x00, 0x00, 0x00, 0x40};
|
||||||
#define GUIDE_VISIBLE_ROWS 5
|
#define GUIDE_VISIBLE_ROWS 5
|
||||||
#define TIMELINE_VISIBLE_SECONDS (90.0 * 60.0)
|
#define TIMELINE_VISIBLE_SECONDS (90.0 * 60.0)
|
||||||
#define GUIDE_INFO_WIDTH 390
|
#define GUIDE_INFO_WIDTH 390
|
||||||
#define GUIDE_PREVIEW_WIDTH 430
|
#define GUIDE_PREVIEW_WIDTH 240
|
||||||
#define GUIDE_PREVIEW_HEIGHT 194
|
#define GUIDE_PREVIEW_HEIGHT 135
|
||||||
|
#define GUIDE_TOP_SECTION_GAP 14
|
||||||
#define GUIDE_STATUS_HEIGHT 42
|
#define GUIDE_STATUS_HEIGHT 42
|
||||||
#define GUIDE_GRID_TOP 212
|
#define GUIDE_GRID_TOP 212
|
||||||
#define GUIDE_FOOTER_HEIGHT 54
|
#define GUIDE_FOOTER_HEIGHT 54
|
||||||
|
|
@ -119,6 +120,12 @@ static const SDL_Color COLOR_PILL_SHADOW = {0x00, 0x00, 0x00, 0x40};
|
||||||
#define GUIDE_X_START 258
|
#define GUIDE_X_START 258
|
||||||
#define GUIDE_INFO_HEIGHT 184
|
#define GUIDE_INFO_HEIGHT 184
|
||||||
|
|
||||||
|
#define CHANNEL_BANNER_HEIGHT 96
|
||||||
|
#define CHANNEL_BANNER_PILL_WIDTH 280
|
||||||
|
#define CHANNEL_BANNER_PADDING_X 18
|
||||||
|
#define CHANNEL_BANNER_PADDING_Y 14
|
||||||
|
#define CHANNEL_BANNER_DIVIDER_GAP 14
|
||||||
|
|
||||||
#define GUIDE_THEME_COUNT 10
|
#define GUIDE_THEME_COUNT 10
|
||||||
|
|
||||||
static const GuideTheme GUIDE_THEMES[GUIDE_THEME_COUNT] = {
|
static const GuideTheme GUIDE_THEMES[GUIDE_THEME_COUNT] = {
|
||||||
|
|
@ -360,7 +367,7 @@ static const GuideTheme GUIDE_THEMES[GUIDE_THEME_COUNT] = {
|
||||||
.block_active_mid = {0x33,0xff,0x00,0xff},
|
.block_active_mid = {0x33,0xff,0x00,0xff},
|
||||||
.block_active_bottom = {0x15,0x6e,0x15,0xff},
|
.block_active_bottom = {0x15,0x6e,0x15,0xff},
|
||||||
.block_text = {0x33,0xff,0x00,0xff},
|
.block_text = {0x33,0xff,0x00,0xff},
|
||||||
.block_active_text = {0xf5,0xf5,0xf0,0xff},
|
.block_active_text = {0x00,0x00,0x00,0xff},
|
||||||
.block_border = {0x4a,0x8a,0x4a,0xff},
|
.block_border = {0x4a,0x8a,0x4a,0xff},
|
||||||
.block_active_border = {0x6a,0xff,0x4a,0xff},
|
.block_active_border = {0x6a,0xff,0x4a,0xff},
|
||||||
.selection_edge = {0x33,0xff,0x00,0xff},
|
.selection_edge = {0x33,0xff,0x00,0xff},
|
||||||
|
|
|
||||||
151
src/ui.c
151
src/ui.c
|
|
@ -1,6 +1,7 @@
|
||||||
#include "ui.h"
|
#include "ui.h"
|
||||||
|
|
||||||
#include <SDL2/SDL_image.h>
|
#include <SDL2/SDL_image.h>
|
||||||
|
#include <SDL2/SDL_filesystem.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
|
@ -194,13 +195,21 @@ static void draw_gloss_line(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Co
|
||||||
}
|
}
|
||||||
|
|
||||||
static void draw_panel_bevel(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color gloss_color) {
|
static void draw_panel_bevel(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color gloss_color) {
|
||||||
SDL_Rect top_white = {rect->x + 8, rect->y, SDL_max(rect->w - 16, 0), 1};
|
SDL_Rect top_highlight = {rect->x + 4, rect->y + 1, SDL_max(rect->w - 8, 0), 2};
|
||||||
SDL_Rect top_black = {rect->x + 8, rect->y + 1, SDL_max(rect->w - 16, 0), 1};
|
SDL_Rect left_highlight = {rect->x + 1, rect->y + 4, 2, SDL_max(rect->h - 8, 0)};
|
||||||
|
SDL_Rect inner_shadow_top = {rect->x + 4, rect->y + 3, SDL_max(rect->w - 8, 0), 1};
|
||||||
|
SDL_Rect right_shadow = {rect->x + rect->w - 3, rect->y + 3, 2, SDL_max(rect->h - 6, 0)};
|
||||||
|
SDL_Rect bottom_shadow = {rect->x + 3, rect->y + rect->h - 3, SDL_max(rect->w - 6, 0), 2};
|
||||||
|
|
||||||
if (top_white.w > 0) {
|
if (!rect || rect->w <= 6 || rect->h <= 6) {
|
||||||
fill_rect_alpha(renderer, &top_white, color_with_alpha(gloss_color, 72));
|
return;
|
||||||
fill_rect_alpha(renderer, &top_black, (SDL_Color){0, 0, 0, 28});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fill_rect_alpha(renderer, &top_highlight, color_with_alpha(gloss_color, 96));
|
||||||
|
fill_rect_alpha(renderer, &left_highlight, color_with_alpha(gloss_color, 64));
|
||||||
|
fill_rect_alpha(renderer, &inner_shadow_top, (SDL_Color){0, 0, 0, 34});
|
||||||
|
fill_rect_alpha(renderer, &right_shadow, (SDL_Color){0, 0, 0, 56});
|
||||||
|
fill_rect_alpha(renderer, &bottom_shadow, (SDL_Color){0, 0, 0, 64});
|
||||||
}
|
}
|
||||||
|
|
||||||
static void stroke_rect_alpha(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color) {
|
static void stroke_rect_alpha(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color) {
|
||||||
|
|
@ -502,8 +511,14 @@ static void draw_mini_status_bar(SDL_Renderer *renderer,
|
||||||
const Channel *selected_channel,
|
const Channel *selected_channel,
|
||||||
const SDL_Rect *rect,
|
const SDL_Rect *rect,
|
||||||
time_t now_wall) {
|
time_t now_wall) {
|
||||||
char status_text[256];
|
char clock_text[32];
|
||||||
|
char channel_text[192];
|
||||||
|
char fitted_channel_text[192];
|
||||||
SDL_Color text_color;
|
SDL_Color text_color;
|
||||||
|
SDL_Rect time_rect;
|
||||||
|
SDL_Rect channel_rect;
|
||||||
|
int time_width = 0;
|
||||||
|
int gap = 8;
|
||||||
|
|
||||||
if (!rect) {
|
if (!rect) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -517,18 +532,33 @@ static void draw_mini_status_bar(SDL_Renderer *renderer,
|
||||||
theme->status_mid,
|
theme->status_mid,
|
||||||
blend_color(theme->status_bottom, theme->status_mid, 220));
|
blend_color(theme->status_bottom, theme->status_mid, 220));
|
||||||
stroke_rect(renderer, rect, theme->panel_border);
|
stroke_rect(renderer, rect, theme->panel_border);
|
||||||
|
draw_panel_bevel(renderer, rect, theme->gloss);
|
||||||
|
|
||||||
if (selected_channel) {
|
|
||||||
char clock_text[32];
|
|
||||||
format_time_compact(clock_text, sizeof(clock_text), now_wall);
|
format_time_compact(clock_text, sizeof(clock_text), now_wall);
|
||||||
snprintf(status_text, sizeof(status_text), "Time %s | Channel %s %d", clock_text, selected_channel->name, selected_channel->number);
|
if (TTF_SizeUTF8(font, clock_text, &time_width, NULL) != 0) {
|
||||||
} else {
|
time_width = rect->w / 3;
|
||||||
char clock_text[32];
|
|
||||||
format_time_compact(clock_text, sizeof(clock_text), now_wall);
|
|
||||||
snprintf(status_text, sizeof(status_text), "Time %s", clock_text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_text_clipped(renderer, font, status_text, rect, rect->x + 10, rect->y + 4, text_color);
|
time_rect = (SDL_Rect){rect->x + 8, rect->y, SDL_min(time_width + 4, rect->w - 16), rect->h};
|
||||||
|
draw_text_clipped(renderer, font, clock_text, &time_rect, time_rect.x, rect->y + 4, text_color);
|
||||||
|
|
||||||
|
if (selected_channel) {
|
||||||
|
channel_rect = (SDL_Rect){time_rect.x + time_rect.w + gap,
|
||||||
|
rect->y,
|
||||||
|
rect->x + rect->w - (time_rect.x + time_rect.w + gap) - 8,
|
||||||
|
rect->h};
|
||||||
|
if (channel_rect.w > 0) {
|
||||||
|
snprintf(channel_text, sizeof(channel_text), "%d %s", selected_channel->number, selected_channel->name);
|
||||||
|
fit_text_with_ellipsis(font, channel_text, channel_rect.w, fitted_channel_text, sizeof(fitted_channel_text));
|
||||||
|
draw_text_clipped(renderer,
|
||||||
|
font,
|
||||||
|
fitted_channel_text,
|
||||||
|
&channel_rect,
|
||||||
|
channel_rect.x,
|
||||||
|
rect->y + 4,
|
||||||
|
text_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void draw_info_panel(SDL_Renderer *renderer,
|
static void draw_info_panel(SDL_Renderer *renderer,
|
||||||
|
|
@ -557,6 +587,7 @@ static void draw_info_panel(SDL_Renderer *renderer,
|
||||||
|
|
||||||
fill_rect(renderer, rect, theme->panel_fill);
|
fill_rect(renderer, rect, theme->panel_fill);
|
||||||
stroke_rect(renderer, rect, theme->panel_border);
|
stroke_rect(renderer, rect, theme->panel_border);
|
||||||
|
draw_panel_bevel(renderer, rect, theme->gloss);
|
||||||
accent = (SDL_Rect){rect->x + 1, rect->y + 1, rect->w - 2, 36};
|
accent = (SDL_Rect){rect->x + 1, rect->y + 1, rect->w - 2, 36};
|
||||||
fill_rect(renderer, &accent, blend_color(theme->panel_fill, theme->ribbon_mid, 40));
|
fill_rect(renderer, &accent, blend_color(theme->panel_fill, theme->ribbon_mid, 40));
|
||||||
|
|
||||||
|
|
@ -633,6 +664,7 @@ static void draw_footer_legend(SDL_Renderer *renderer,
|
||||||
blend_color(theme->footer_bottom, theme->footer_mid, 240));
|
blend_color(theme->footer_bottom, theme->footer_mid, 240));
|
||||||
draw_gloss_line(renderer, &footer, color_with_alpha(theme->gloss, 20));
|
draw_gloss_line(renderer, &footer, color_with_alpha(theme->gloss, 20));
|
||||||
stroke_rect(renderer, &footer, theme->panel_border);
|
stroke_rect(renderer, &footer, theme->panel_border);
|
||||||
|
draw_panel_bevel(renderer, &footer, theme->gloss);
|
||||||
|
|
||||||
draw_pill_button(renderer, theme, &chip, theme->panel_fill, theme->panel_border);
|
draw_pill_button(renderer, theme, &chip, theme->panel_fill, theme->panel_border);
|
||||||
draw_text_clipped(renderer, fonts->small, "A", &footer, chip.x + 11, chip.y + 2, footer_text);
|
draw_text_clipped(renderer, fonts->small, "A", &footer, chip.x + 11, chip.y + 2, footer_text);
|
||||||
|
|
@ -660,13 +692,6 @@ static void draw_footer_legend(SDL_Renderer *renderer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void draw_scanline_overlay(SDL_Renderer *renderer, int width, int height, const GuideTheme *theme) {
|
|
||||||
(void) renderer;
|
|
||||||
(void) width;
|
|
||||||
(void) height;
|
|
||||||
(void) theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void draw_channel_status_banner(SDL_Renderer *renderer,
|
static void draw_channel_status_banner(SDL_Renderer *renderer,
|
||||||
const UiFonts *fonts,
|
const UiFonts *fonts,
|
||||||
const GuideTheme *theme,
|
const GuideTheme *theme,
|
||||||
|
|
@ -682,6 +707,9 @@ static void draw_channel_status_banner(SDL_Renderer *renderer,
|
||||||
SDL_Rect banner;
|
SDL_Rect banner;
|
||||||
SDL_Rect channel_pill;
|
SDL_Rect channel_pill;
|
||||||
SDL_Rect info_clip;
|
SDL_Rect info_clip;
|
||||||
|
int banner_padding_x;
|
||||||
|
int banner_padding_y;
|
||||||
|
int divider_x;
|
||||||
SDL_Color banner_text;
|
SDL_Color banner_text;
|
||||||
SDL_Color sub_text;
|
SDL_Color sub_text;
|
||||||
char channel_text[96];
|
char channel_text[96];
|
||||||
|
|
@ -707,13 +735,18 @@ static void draw_channel_status_banner(SDL_Renderer *renderer,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
banner = (SDL_Rect){window_width / 2 - 360, window_height - 92, 720, 64};
|
banner_padding_x = CHANNEL_BANNER_PADDING_X;
|
||||||
if (banner.x < 24) {
|
banner_padding_y = CHANNEL_BANNER_PADDING_Y;
|
||||||
banner.x = 24;
|
banner = (SDL_Rect){0, window_height - CHANNEL_BANNER_HEIGHT, window_width, CHANNEL_BANNER_HEIGHT};
|
||||||
banner.w = window_width - 48;
|
channel_pill = (SDL_Rect){banner.x + banner_padding_x,
|
||||||
}
|
banner.y + banner_padding_y,
|
||||||
channel_pill = (SDL_Rect){banner.x + 10, banner.y + 10, 210, banner.h - 20};
|
CHANNEL_BANNER_PILL_WIDTH,
|
||||||
info_clip = (SDL_Rect){channel_pill.x + channel_pill.w + 14, banner.y + 8, banner.w - channel_pill.w - 28, banner.h - 16};
|
banner.h - (banner_padding_y * 2)};
|
||||||
|
divider_x = channel_pill.x + channel_pill.w + CHANNEL_BANNER_DIVIDER_GAP;
|
||||||
|
info_clip = (SDL_Rect){divider_x + CHANNEL_BANNER_DIVIDER_GAP,
|
||||||
|
banner.y + banner_padding_y,
|
||||||
|
banner.w - (divider_x + CHANNEL_BANNER_DIVIDER_GAP) - banner_padding_x,
|
||||||
|
banner.h - (banner_padding_y * 2)};
|
||||||
banner_text = ensure_contrast(theme->ribbon_text, theme->status_mid);
|
banner_text = ensure_contrast(theme->ribbon_text, theme->status_mid);
|
||||||
sub_text = ensure_contrast(theme->row_subtext, theme->status_mid);
|
sub_text = ensure_contrast(theme->row_subtext, theme->status_mid);
|
||||||
|
|
||||||
|
|
@ -734,6 +767,7 @@ static void draw_channel_status_banner(SDL_Renderer *renderer,
|
||||||
color_with_alpha(theme->gloss, 42),
|
color_with_alpha(theme->gloss, 42),
|
||||||
theme->panel_border);
|
theme->panel_border);
|
||||||
stroke_rect(renderer, &banner, theme->panel_border);
|
stroke_rect(renderer, &banner, theme->panel_border);
|
||||||
|
draw_panel_bevel(renderer, &banner, theme->gloss);
|
||||||
|
|
||||||
if (numeric_input_length > 0 || numeric_input_invalid) {
|
if (numeric_input_length > 0 || numeric_input_invalid) {
|
||||||
SDL_Color accent_fill = theme->ribbon_mid;
|
SDL_Color accent_fill = theme->ribbon_mid;
|
||||||
|
|
@ -769,23 +803,23 @@ static void draw_channel_status_banner(SDL_Renderer *renderer,
|
||||||
|
|
||||||
set_draw_color(renderer, theme->status_divider);
|
set_draw_color(renderer, theme->status_divider);
|
||||||
SDL_RenderDrawLine(renderer,
|
SDL_RenderDrawLine(renderer,
|
||||||
channel_pill.x + channel_pill.w + 6,
|
divider_x,
|
||||||
banner.y + 10,
|
banner.y + banner_padding_y,
|
||||||
channel_pill.x + channel_pill.w + 6,
|
divider_x,
|
||||||
banner.y + banner.h - 10);
|
banner.y + banner.h - banner_padding_y);
|
||||||
draw_text_clipped(renderer,
|
draw_text_clipped(renderer,
|
||||||
fonts->medium,
|
fonts->medium,
|
||||||
program->program_title,
|
program->program_title,
|
||||||
&info_clip,
|
&info_clip,
|
||||||
info_clip.x,
|
info_clip.x,
|
||||||
banner.y + 10,
|
banner.y + 18,
|
||||||
banner_text);
|
banner_text);
|
||||||
draw_text_clipped(renderer,
|
draw_text_clipped(renderer,
|
||||||
fonts->small,
|
fonts->small,
|
||||||
time_range,
|
time_range,
|
||||||
&info_clip,
|
&info_clip,
|
||||||
info_clip.x,
|
info_clip.x,
|
||||||
banner.y + 36,
|
banner.y + 52,
|
||||||
sub_text);
|
sub_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -925,11 +959,13 @@ void ui_render_guide(SDL_Renderer *renderer,
|
||||||
int guide_x_start = (int) (GUIDE_X_START * scale_x);
|
int guide_x_start = (int) (GUIDE_X_START * scale_x);
|
||||||
int sidebar_width = (int) (GUIDE_SIDEBAR_WIDTH * scale_x);
|
int sidebar_width = (int) (GUIDE_SIDEBAR_WIDTH * scale_x);
|
||||||
int mini_status_height = (int) (26 * scale_y);
|
int mini_status_height = (int) (26 * scale_y);
|
||||||
|
int top_section_gap = (int) (GUIDE_TOP_SECTION_GAP * scale_y);
|
||||||
SDL_Rect full = {0, 0, window_width, window_height};
|
SDL_Rect full = {0, 0, window_width, window_height};
|
||||||
SDL_Rect info_panel = {0, 0, (int) (GUIDE_INFO_WIDTH * scale_x), (int) (GUIDE_INFO_HEIGHT * scale_y)};
|
SDL_Rect info_panel = {0, 0, (int) (GUIDE_INFO_WIDTH * scale_x), (int) (GUIDE_INFO_HEIGHT * scale_y)};
|
||||||
SDL_Rect preview = {window_width - (int) (GUIDE_PREVIEW_WIDTH * scale_x), 0, (int) (GUIDE_PREVIEW_WIDTH * scale_x), (int) (GUIDE_PREVIEW_HEIGHT * scale_y)};
|
SDL_Rect preview = {window_width - (int) (GUIDE_PREVIEW_WIDTH * scale_x), 0, (int) (GUIDE_PREVIEW_WIDTH * scale_x), (int) (GUIDE_PREVIEW_HEIGHT * scale_y)};
|
||||||
int mini_status_y = preview.y + preview.h;
|
int mini_status_y = preview.y + preview.h;
|
||||||
int timeline_header_y = mini_status_y + mini_status_height;
|
int top_section_bottom = SDL_max(info_panel.y + info_panel.h, mini_status_y + mini_status_height);
|
||||||
|
int timeline_header_y = top_section_bottom + top_section_gap;
|
||||||
int grid_y = timeline_header_y + (int) (GUIDE_STATUS_HEIGHT * scale_y);
|
int grid_y = timeline_header_y + (int) (GUIDE_STATUS_HEIGHT * scale_y);
|
||||||
SDL_Rect mini_status_bar = {preview.x, mini_status_y, preview.w, mini_status_height};
|
SDL_Rect mini_status_bar = {preview.x, mini_status_y, preview.w, mini_status_height};
|
||||||
SDL_Rect header_row = {0, timeline_header_y, window_width, (int) (GUIDE_STATUS_HEIGHT * scale_y)};
|
SDL_Rect header_row = {0, timeline_header_y, window_width, (int) (GUIDE_STATUS_HEIGHT * scale_y)};
|
||||||
|
|
@ -945,6 +981,8 @@ void ui_render_guide(SDL_Renderer *renderer,
|
||||||
double guide_view_end_seconds = guide_view_start_seconds + TIMELINE_VISIBLE_SECONDS;
|
double guide_view_end_seconds = guide_view_start_seconds + TIMELINE_VISIBLE_SECONDS;
|
||||||
double guide_focus_seconds = channel_schedule_elapsed_seconds(app_start_time, guide_focus_time);
|
double guide_focus_seconds = channel_schedule_elapsed_seconds(app_start_time, guide_focus_time);
|
||||||
|
|
||||||
|
(void) active_channel;
|
||||||
|
|
||||||
if (channels && channels->count > 0 && selected_channel >= 0 && selected_channel < channels->count) {
|
if (channels && channels->count > 0 && selected_channel >= 0 && selected_channel < channels->count) {
|
||||||
selected_channel_ptr = &channels->items[selected_channel];
|
selected_channel_ptr = &channels->items[selected_channel];
|
||||||
}
|
}
|
||||||
|
|
@ -957,6 +995,8 @@ void ui_render_guide(SDL_Renderer *renderer,
|
||||||
draw_info_panel(renderer, fonts, theme, selected_channel_ptr, &info_panel, app_start_time, guide_focus_time);
|
draw_info_panel(renderer, fonts, theme, selected_channel_ptr, &info_panel, app_start_time, guide_focus_time);
|
||||||
draw_panel_shadow(renderer, &preview);
|
draw_panel_shadow(renderer, &preview);
|
||||||
fill_rect(renderer, &preview, COLOR_BLACK);
|
fill_rect(renderer, &preview, COLOR_BLACK);
|
||||||
|
stroke_rect(renderer, &preview, theme->panel_border);
|
||||||
|
draw_panel_bevel(renderer, &preview, theme->gloss);
|
||||||
draw_video(renderer, video_texture, texture_width, texture_height, preview);
|
draw_video(renderer, video_texture, texture_width, texture_height, preview);
|
||||||
draw_mini_status_bar(renderer, fonts->small, theme, selected_channel_ptr, &mini_status_bar, now_wall);
|
draw_mini_status_bar(renderer, fonts->small, theme, selected_channel_ptr, &mini_status_bar, now_wall);
|
||||||
|
|
||||||
|
|
@ -1144,7 +1184,6 @@ void ui_render_guide(SDL_Renderer *renderer,
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_footer_legend(renderer, fonts, theme, window_width, window_height, cache->logo_texture);
|
draw_footer_legend(renderer, fonts, theme, window_width, window_height, cache->logo_texture);
|
||||||
draw_scanline_overlay(renderer, window_width, window_height, theme);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ui_render_theme_picker(SDL_Renderer *renderer,
|
void ui_render_theme_picker(SDL_Renderer *renderer,
|
||||||
|
|
@ -1198,6 +1237,8 @@ void ui_render_theme_picker(SDL_Renderer *renderer,
|
||||||
active_theme->panel_text);
|
active_theme->panel_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static char *get_exe_dir(void);
|
||||||
|
|
||||||
int ui_cache_init(UiCache *cache, SDL_Renderer *renderer, const UiFonts *fonts, const ChannelList *channels) {
|
int ui_cache_init(UiCache *cache, SDL_Renderer *renderer, const UiFonts *fonts, const ChannelList *channels) {
|
||||||
char buffer[256];
|
char buffer[256];
|
||||||
|
|
||||||
|
|
@ -1214,7 +1255,7 @@ int ui_cache_init(UiCache *cache, SDL_Renderer *renderer, const UiFonts *fonts,
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text_texture_init(&cache->no_media_title, renderer, fonts->large, "Passport-C Media Player", COLOR_TEXT_LIGHT) != 0 ||
|
if (text_texture_init(&cache->no_media_title, renderer, fonts->large, "FreePassport-C Media Player", COLOR_TEXT_LIGHT) != 0 ||
|
||||||
text_texture_init(&cache->no_media_body, renderer, fonts->medium, "No channels found in ./media", COLOR_HIGHLIGHT_YELLOW) != 0 ||
|
text_texture_init(&cache->no_media_body, renderer, fonts->medium, "No channels found in ./media", COLOR_HIGHLIGHT_YELLOW) != 0 ||
|
||||||
text_texture_init(&cache->no_media_hint, renderer, fonts->small, "Add MP4 or MKV files to ./media and relaunch.", COLOR_PALE_BLUE) != 0 ||
|
text_texture_init(&cache->no_media_hint, renderer, fonts->small, "Add MP4 or MKV files to ./media and relaunch.", COLOR_PALE_BLUE) != 0 ||
|
||||||
text_texture_init(&cache->footer_a, renderer, fonts->medium, "A", COLOR_TEXT_DARK) != 0 ||
|
text_texture_init(&cache->footer_a, renderer, fonts->medium, "A", COLOR_TEXT_DARK) != 0 ||
|
||||||
|
|
@ -1227,7 +1268,19 @@ int ui_cache_init(UiCache *cache, SDL_Renderer *renderer, const UiFonts *fonts,
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
char *exe_dir = get_exe_dir();
|
||||||
|
if (exe_dir) {
|
||||||
|
char logo_path[512];
|
||||||
|
snprintf(logo_path, sizeof(logo_path), "%slogo.png", exe_dir);
|
||||||
|
cache->logo_texture = load_png_texture(renderer, logo_path, NULL, NULL);
|
||||||
|
SDL_free(exe_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cache->logo_texture) {
|
||||||
cache->logo_texture = load_png_texture(renderer, "logo.png", NULL, NULL);
|
cache->logo_texture = load_png_texture(renderer, "logo.png", NULL, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (channels && channels->count > 0) {
|
if (channels && channels->count > 0) {
|
||||||
cache->channels = calloc((size_t) channels->count, sizeof(UiChannelCache));
|
cache->channels = calloc((size_t) channels->count, sizeof(UiChannelCache));
|
||||||
|
|
@ -1302,12 +1355,36 @@ void ui_cache_destroy(UiCache *cache) {
|
||||||
memset(cache, 0, sizeof(*cache));
|
memset(cache, 0, sizeof(*cache));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static char *get_exe_dir(void) {
|
||||||
|
return SDL_GetBasePath();
|
||||||
|
}
|
||||||
|
|
||||||
int ui_load_fonts(UiFonts *fonts) {
|
int ui_load_fonts(UiFonts *fonts) {
|
||||||
|
char *exe_dir;
|
||||||
|
|
||||||
if (!fonts) {
|
if (!fonts) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
memset(fonts, 0, sizeof(*fonts));
|
memset(fonts, 0, sizeof(*fonts));
|
||||||
|
|
||||||
|
exe_dir = get_exe_dir();
|
||||||
|
if (exe_dir) {
|
||||||
|
char font_path[512];
|
||||||
|
snprintf(font_path, sizeof(font_path), "%sBigBlueTermPlusNerdFontMono-Regular.ttf", exe_dir);
|
||||||
|
|
||||||
|
fonts->small = TTF_OpenFont(font_path, 13);
|
||||||
|
fonts->medium = TTF_OpenFont(font_path, 17);
|
||||||
|
fonts->large = TTF_OpenFont(font_path, 22);
|
||||||
|
|
||||||
|
SDL_free(exe_dir);
|
||||||
|
|
||||||
|
if (fonts->small && fonts->medium && fonts->large) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
ui_destroy_fonts(fonts);
|
||||||
|
}
|
||||||
|
|
||||||
for (size_t i = 0; i < sizeof(FONT_CANDIDATES) / sizeof(FONT_CANDIDATES[0]); ++i) {
|
for (size_t i = 0; i < sizeof(FONT_CANDIDATES) / sizeof(FONT_CANDIDATES[0]); ++i) {
|
||||||
fonts->small = TTF_OpenFont(FONT_CANDIDATES[i], 13);
|
fonts->small = TTF_OpenFont(FONT_CANDIDATES[i], 13);
|
||||||
fonts->medium = TTF_OpenFont(FONT_CANDIDATES[i], 17);
|
fonts->medium = TTF_OpenFont(FONT_CANDIDATES[i], 17);
|
||||||
|
|
|
||||||
4
src/ui.d
Normal file
4
src/ui.d
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
src/ui.o: src/ui.c src/ui.h src/channel.h src/theme.h
|
||||||
|
src/ui.h:
|
||||||
|
src/channel.h:
|
||||||
|
src/theme.h:
|
||||||
Loading…
Add table
Add a link
Reference in a new issue