diff --git a/Makefile b/Makefile index 22334cc..dcb5403 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ CC ?= cc CSTD ?= -std=c11 CFLAGS ?= -O2 -Wall -Wextra -Wpedantic $(CSTD) +CFLAGS += -MMD -MP 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) @@ -19,6 +20,7 @@ endif SRC := $(wildcard src/*.c) OBJ := $(SRC:.c=.o) +DEP := $(OBJ:.o=.d) BIN := passport-c-media-player CPPFLAGS += -I./src $(SDL_CFLAGS) $(FFMPEG_CFLAGS) @@ -31,6 +33,8 @@ all: $(BIN) $(BIN): $(OBJ) $(CC) $(OBJ) -o $@ $(LDLIBS) +-include $(DEP) + run: $(BIN) ./$(BIN) diff --git a/README.md b/README.md index f675a05..776083e 100644 --- a/README.md +++ b/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 @@ -21,6 +21,7 @@ Passport-C Media Player is a low-dependency SDL2 + FFmpeg prototype that recreat - `Up` / `Down` - surf channels - `Tab` - toggle guide +- `i` - show current channel info - `Esc` - exit guide or quit the app ## Build @@ -48,10 +49,17 @@ To create a portable AppImage that can run on any Linux distribution: 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 ./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. diff --git a/build-appimage.sh b/build-appimage.sh index 1d11705..4dfb307 100755 --- a/build-appimage.sh +++ b/build-appimage.sh @@ -2,9 +2,8 @@ set -e -APP_NAME="Passport-C-Media-Player" +APP_NAME="FreePassport-C-Media-Player" APP_VERSION="0.1" -OUTPUT_NAME="${APP_NAME}-${APP_VERSION}-x86_64.AppImage" # Colors for output RED='\033[0;31m' @@ -12,9 +11,29 @@ GREEN='\033[0;32m' YELLOW='\033[1;33m' 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/ -echo -e "${GREEN}Building ${APP_NAME} AppImage...${NC}" +echo -e "${GREEN}Building ${APP_NAME} AppImage for ${ARCH}...${NC}" # Check if binary exists 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/ 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) cat > AppDir/passport-c-media-player.desktop << 'EOF' [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 Exec=passport-c-media-player Icon=passport-c-media-player @@ -63,15 +90,15 @@ ln -sf usr/bin/passport-c-media-player AppRun cd .. # Download linuxdeploy if not present -if [ ! -f "linuxdeploy-x86_64.AppImage" ]; then - echo -e "${GREEN}Downloading linuxdeploy...${NC}" - wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage - chmod +x linuxdeploy-x86_64.AppImage +if [ ! -f "${LINUXDEPLOY_BIN}" ]; then + echo -e "${GREEN}Downloading linuxdeploy for ${ARCH}...${NC}" + wget -q "${LINUXDEPLOY_URL}" -O "${LINUXDEPLOY_BIN}" + chmod +x "${LINUXDEPLOY_BIN}" fi # Build AppImage -echo -e "${GREEN}Creating AppImage...${NC}" -./linuxdeploy-x86_64.AppImage \ +echo -e "${GREEN}Creating AppImage for ${ARCH}...${NC}" +./"${LINUXDEPLOY_BIN}" \ --appdir AppDir \ --output appimage \ -d AppDir/usr/share/applications/passport-c-media-player.desktop \ @@ -79,7 +106,7 @@ echo -e "${GREEN}Creating AppImage...${NC}" # Rename output file # 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 mv "$file" "${OUTPUT_NAME}" break diff --git a/screenshotguide.png b/screenshotguide.png deleted file mode 100644 index 4ed963b..0000000 Binary files a/screenshotguide.png and /dev/null differ diff --git a/src/app.c b/src/app.c index c67c289..5075bb4 100644 --- a/src/app.c +++ b/src/app.c @@ -15,6 +15,7 @@ #define CHANNEL_BANNER_DURATION_MS 5000 #define CHANNEL_SWITCH_LOCK_DURATION_MS 7000 #define NUMERIC_INPUT_TIMEOUT_MS 1000 +#define GUIDE_PREVIEW_FRAME_INTERVAL_MS 83 static void configure_runtime_environment(void) { char runtime_dir[64]; @@ -66,6 +67,9 @@ static void destroy_video_texture(App *app) { app->texture_width = 0; app->texture_height = 0; } + if (app) { + app->last_guide_preview_update = 0; + } } static void begin_startup_handoff(App *app) { @@ -82,8 +86,25 @@ static int update_video_texture(App *app) { double sync_clock; double lead_tolerance; Uint64 now_ticks; + Uint32 update_ticks; + + if (!app) { + return -1; + } 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); 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_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) { app->startup_handoff_active = 0; app->startup_handoff_until = 0; @@ -374,6 +399,11 @@ static void handle_event(App *app, const SDL_Event *event) { tune_relative(app, -1); } break; + case SDLK_i: + if (app->mode == MODE_FULLSCREEN) { + app->channel_banner_until = SDL_GetTicks() + CHANNEL_BANNER_DURATION_MS; + } + break; case SDLK_LEFT: browse_guide_time(app, -GUIDE_BROWSE_STEP_MINUTES); break; @@ -423,7 +453,7 @@ int app_init(App *app) { return -1; } - app->window = SDL_CreateWindow("Passport-C Media Player", + app->window = SDL_CreateWindow("FreePassport-C Media Player", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WINDOW_WIDTH, diff --git a/src/app.d b/src/app.d new file mode 100644 index 0000000..708163b --- /dev/null +++ b/src/app.d @@ -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: diff --git a/src/app.h b/src/app.h index f8d6912..5207292 100644 --- a/src/app.h +++ b/src/app.h @@ -24,6 +24,7 @@ typedef struct App { int startup_handoff_active; int last_blackout_state; Uint32 startup_handoff_until; + Uint32 last_guide_preview_update; Uint32 channel_banner_until; Uint32 channel_switch_lock_until; time_t app_start_time; diff --git a/src/channel.d b/src/channel.d new file mode 100644 index 0000000..644d8f4 --- /dev/null +++ b/src/channel.d @@ -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: diff --git a/src/frame_queue.c b/src/frame_queue.c index 0a49fd7..562e9d4 100644 --- a/src/frame_queue.c +++ b/src/frame_queue.c @@ -9,10 +9,114 @@ void frame_data_free(FrameData *frame) { return; } - av_freep(&frame->buffer); + if (frame->pool && frame->buffer) { + video_buffer_pool_release(frame->pool, frame->buffer); + } 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) { if (!queue) { return -1; diff --git a/src/frame_queue.d b/src/frame_queue.d new file mode 100644 index 0000000..6e86a35 --- /dev/null +++ b/src/frame_queue.d @@ -0,0 +1,2 @@ +src/frame_queue.o: src/frame_queue.c src/frame_queue.h +src/frame_queue.h: diff --git a/src/frame_queue.h b/src/frame_queue.h index d4cdf1e..f4ec35a 100644 --- a/src/frame_queue.h +++ b/src/frame_queue.h @@ -1,12 +1,17 @@ #ifndef FRAME_QUEUE_H #define FRAME_QUEUE_H +#include #include -#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 { unsigned char *buffer; + struct VideoBufferPool *pool; unsigned char *plane_y; unsigned char *plane_u; unsigned char *plane_v; @@ -26,6 +31,13 @@ typedef struct FrameQueue { SDL_cond *cond; } 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); void frame_queue_destroy(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_pop_first(FrameQueue *queue, FrameData *out); 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 diff --git a/src/main.d b/src/main.d new file mode 100644 index 0000000..fd0decd --- /dev/null +++ b/src/main.d @@ -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: diff --git a/src/player.c b/src/player.c index e30f03c..32d9c90 100644 --- a/src/player.c +++ b/src/player.c @@ -15,6 +15,8 @@ #include #include +#define AUDIO_BUFFER_SAMPLES 48000 + typedef struct DecoderThreadArgs { Player *player; const Channel *channel; @@ -180,13 +182,13 @@ static int player_ensure_audio_device(Player *player) { } static int queue_audio_frame(Player *player, - SwrContext **swr_context, - AVCodecContext *audio_codec_context, - AVRational audio_time_base, - AVFrame *audio_frame, - double pts_base_offset, - int *queued_any_audio) { - uint8_t **converted_data = NULL; + SwrContext **swr_context, + AVCodecContext *audio_codec_context, + AVRational audio_time_base, + AVFrame *audio_frame, + double pts_base_offset, + int *queued_any_audio) { + uint8_t *converted_data[1] = {NULL}; int out_rate; int out_channels; enum AVSampleFormat out_format; @@ -196,10 +198,10 @@ static int queue_audio_frame(Player *player, int sample_count; int buffer_size; int queued_limit; - int line_size = 0; int result; double audio_pts; double frame_duration; + int wait_ms = 2; if (!player || !audio_codec_context || !audio_frame) { return -1; @@ -236,34 +238,34 @@ static int queue_audio_frame(Player *player, audio_codec_context->sample_rate, AV_ROUND_UP); - if (av_samples_alloc_array_and_samples(&converted_data, - &line_size, - out_channels, - max_samples, - out_format, - 0) < 0) { - av_channel_layout_uninit(&out_layout); - player_set_error(player, "Unable to allocate audio buffer"); - return -1; + if (max_samples > player->audio_convert_buffer_samples) { + int new_size = max_samples * out_channels * sizeof(int16_t); + uint8_t *new_buffer = (uint8_t *)av_realloc(player->audio_convert_buffer, new_size); + if (!new_buffer) { + av_channel_layout_uninit(&out_layout); + player_set_error(player, "Unable to resize audio buffer"); + 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, converted_data, max_samples, (const uint8_t * const *) audio_frame->extended_data, audio_frame->nb_samples); if (sample_count < 0) { - av_freep(&converted_data[0]); - av_freep(&converted_data); av_channel_layout_uninit(&out_layout); player_set_error(player, "Audio resample failed"); 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) { - av_freep(&converted_data[0]); - av_freep(&converted_data); av_channel_layout_uninit(&out_layout); return 0; } @@ -275,12 +277,11 @@ static int queue_audio_frame(Player *player, queued_limit = (int) (player_audio_bytes_per_second(player) * 0.18); 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); - av_freep(&converted_data[0]); - av_freep(&converted_data); + result = SDL_QueueAudio(player->audio_device, player->audio_convert_buffer, (Uint32) buffer_size); if (result != 0) { 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->video_codec_context->height, ctx->video_codec_context->pix_fmt, - ctx->video_codec_context->width, - ctx->video_codec_context->height, + TARGET_WIDTH, + TARGET_HEIGHT, AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL, @@ -612,30 +613,62 @@ static int decode_thread_main(void *userdata) { FrameData frame = {0}; uint8_t *dest_data[4] = {0}; int dest_linesize[4] = {0}; - int image_size; + int needs_conversion = 1; - frame.width = ctx.video_codec_context->width; - frame.height = ctx.video_codec_context->height; + while (!should_stop(player) && frame_queue_is_full(&player->frame_queue)) { + SDL_Delay(5); + } + if (should_stop(player)) { + break; + } + + frame.width = TARGET_WIDTH; + frame.height = TARGET_HEIGHT; 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) { - player_set_error(player, "Unable to allocate frame buffer"); + + frame.buffer = video_buffer_pool_acquire(&player->video_buffer_pool); + if (!frame.buffer) { + player_set_error(player, "Unable to acquire frame buffer from pool"); goto cleanup; } - frame.buffer = dest_data[0]; + + frame.pool = &player->video_buffer_pool; + dest_data[0] = frame.buffer; + dest_data[1] = frame.buffer + (TARGET_WIDTH * TARGET_HEIGHT); + dest_data[2] = frame.buffer + (TARGET_WIDTH * TARGET_HEIGHT * 5 / 4); + dest_linesize[0] = TARGET_WIDTH; + 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, + (const uint8_t *const *) ctx.decoded_frame->data, + ctx.decoded_frame->linesize, + 0, + ctx.video_codec_context->height, + dest_data, + 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]; - sws_scale(ctx.sws_context, - (const uint8_t *const *) ctx.decoded_frame->data, - ctx.decoded_frame->linesize, - 0, - ctx.video_codec_context->height, - dest_data, - dest_linesize); + frame_queue_push(&player->frame_queue, &frame); queued_first_video = 1; MAYBE_MARK_PREROLL_READY(); @@ -696,6 +729,28 @@ int player_init(Player *player, const ChannelList *channels, Uint64 app_start_ti 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; } @@ -706,6 +761,10 @@ void player_destroy(Player *player) { player_stop_thread(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); if (player->audio_mutex) { SDL_DestroyMutex(player->audio_mutex); diff --git a/src/player.d b/src/player.d new file mode 100644 index 0000000..8d072b6 --- /dev/null +++ b/src/player.d @@ -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: diff --git a/src/player.h b/src/player.h index 0568cf6..a9fe1be 100644 --- a/src/player.h +++ b/src/player.h @@ -1,6 +1,7 @@ #ifndef PLAYER_H #define PLAYER_H +#include #include #include @@ -9,6 +10,7 @@ typedef struct Player { FrameQueue frame_queue; + VideoBufferPool video_buffer_pool; SDL_Thread *thread; SDL_atomic_t stop_requested; const ChannelList *channels; @@ -27,6 +29,9 @@ typedef struct Player { char audio_driver[32]; SDL_mutex *error_mutex; char last_error[256]; + uint8_t *audio_convert_buffer; + int audio_convert_buffer_size; + int audio_convert_buffer_samples; } Player; int player_init(Player *player, const ChannelList *channels, Uint64 app_start_ticks); diff --git a/src/theme.h b/src/theme.h index f321b30..92fbc48 100644 --- a/src/theme.h +++ b/src/theme.h @@ -110,8 +110,9 @@ static const SDL_Color COLOR_PILL_SHADOW = {0x00, 0x00, 0x00, 0x40}; #define GUIDE_VISIBLE_ROWS 5 #define TIMELINE_VISIBLE_SECONDS (90.0 * 60.0) #define GUIDE_INFO_WIDTH 390 -#define GUIDE_PREVIEW_WIDTH 430 -#define GUIDE_PREVIEW_HEIGHT 194 +#define GUIDE_PREVIEW_WIDTH 240 +#define GUIDE_PREVIEW_HEIGHT 135 +#define GUIDE_TOP_SECTION_GAP 14 #define GUIDE_STATUS_HEIGHT 42 #define GUIDE_GRID_TOP 212 #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_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 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_bottom = {0x15,0x6e,0x15,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_active_border = {0x6a,0xff,0x4a,0xff}, .selection_edge = {0x33,0xff,0x00,0xff}, diff --git a/src/ui.c b/src/ui.c index b7d5416..2480a54 100644 --- a/src/ui.c +++ b/src/ui.c @@ -1,6 +1,7 @@ #include "ui.h" #include +#include #include #include @@ -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) { - SDL_Rect top_white = {rect->x + 8, rect->y, SDL_max(rect->w - 16, 0), 1}; - SDL_Rect top_black = {rect->x + 8, rect->y + 1, 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 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) { - fill_rect_alpha(renderer, &top_white, color_with_alpha(gloss_color, 72)); - fill_rect_alpha(renderer, &top_black, (SDL_Color){0, 0, 0, 28}); + if (!rect || rect->w <= 6 || rect->h <= 6) { + return; } + + 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) { @@ -502,8 +511,14 @@ static void draw_mini_status_bar(SDL_Renderer *renderer, const Channel *selected_channel, const SDL_Rect *rect, 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_Rect time_rect; + SDL_Rect channel_rect; + int time_width = 0; + int gap = 8; if (!rect) { return; @@ -517,18 +532,33 @@ static void draw_mini_status_bar(SDL_Renderer *renderer, theme->status_mid, blend_color(theme->status_bottom, theme->status_mid, 220)); 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); - snprintf(status_text, sizeof(status_text), "Time %s | Channel %s %d", clock_text, selected_channel->name, selected_channel->number); - } else { - char clock_text[32]; - format_time_compact(clock_text, sizeof(clock_text), now_wall); - snprintf(status_text, sizeof(status_text), "Time %s", clock_text); + format_time_compact(clock_text, sizeof(clock_text), now_wall); + if (TTF_SizeUTF8(font, clock_text, &time_width, NULL) != 0) { + time_width = rect->w / 3; } - 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, @@ -557,6 +587,7 @@ static void draw_info_panel(SDL_Renderer *renderer, fill_rect(renderer, rect, theme->panel_fill); 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}; 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)); draw_gloss_line(renderer, &footer, color_with_alpha(theme->gloss, 20)); 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_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, const UiFonts *fonts, const GuideTheme *theme, @@ -682,6 +707,9 @@ static void draw_channel_status_banner(SDL_Renderer *renderer, SDL_Rect banner; SDL_Rect channel_pill; SDL_Rect info_clip; + int banner_padding_x; + int banner_padding_y; + int divider_x; SDL_Color banner_text; SDL_Color sub_text; char channel_text[96]; @@ -707,13 +735,18 @@ static void draw_channel_status_banner(SDL_Renderer *renderer, return; } - banner = (SDL_Rect){window_width / 2 - 360, window_height - 92, 720, 64}; - if (banner.x < 24) { - banner.x = 24; - banner.w = window_width - 48; - } - channel_pill = (SDL_Rect){banner.x + 10, banner.y + 10, 210, banner.h - 20}; - info_clip = (SDL_Rect){channel_pill.x + channel_pill.w + 14, banner.y + 8, banner.w - channel_pill.w - 28, banner.h - 16}; + banner_padding_x = CHANNEL_BANNER_PADDING_X; + banner_padding_y = CHANNEL_BANNER_PADDING_Y; + banner = (SDL_Rect){0, window_height - CHANNEL_BANNER_HEIGHT, window_width, CHANNEL_BANNER_HEIGHT}; + channel_pill = (SDL_Rect){banner.x + banner_padding_x, + banner.y + banner_padding_y, + CHANNEL_BANNER_PILL_WIDTH, + 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); 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), 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) { 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); SDL_RenderDrawLine(renderer, - channel_pill.x + channel_pill.w + 6, - banner.y + 10, - channel_pill.x + channel_pill.w + 6, - banner.y + banner.h - 10); + divider_x, + banner.y + banner_padding_y, + divider_x, + banner.y + banner.h - banner_padding_y); draw_text_clipped(renderer, fonts->medium, program->program_title, &info_clip, info_clip.x, - banner.y + 10, + banner.y + 18, banner_text); draw_text_clipped(renderer, fonts->small, time_range, &info_clip, info_clip.x, - banner.y + 36, + banner.y + 52, sub_text); } @@ -925,11 +959,13 @@ void ui_render_guide(SDL_Renderer *renderer, int guide_x_start = (int) (GUIDE_X_START * scale_x); int sidebar_width = (int) (GUIDE_SIDEBAR_WIDTH * scale_x); 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 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)}; 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); 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)}; @@ -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_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) { 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_panel_shadow(renderer, &preview); 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_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_scanline_overlay(renderer, window_width, window_height, theme); } void ui_render_theme_picker(SDL_Renderer *renderer, @@ -1198,6 +1237,8 @@ void ui_render_theme_picker(SDL_Renderer *renderer, 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) { char buffer[256]; @@ -1214,7 +1255,7 @@ int ui_cache_init(UiCache *cache, SDL_Renderer *renderer, const UiFonts *fonts, 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_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 || @@ -1227,7 +1268,19 @@ int ui_cache_init(UiCache *cache, SDL_Renderer *renderer, const UiFonts *fonts, return -1; } - cache->logo_texture = load_png_texture(renderer, "logo.png", NULL, NULL); + { + 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); + } + } if (channels && channels->count > 0) { cache->channels = calloc((size_t) channels->count, sizeof(UiChannelCache)); @@ -1302,12 +1355,36 @@ void ui_cache_destroy(UiCache *cache) { memset(cache, 0, sizeof(*cache)); } +static char *get_exe_dir(void) { + return SDL_GetBasePath(); +} + int ui_load_fonts(UiFonts *fonts) { + char *exe_dir; + if (!fonts) { return -1; } 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) { fonts->small = TTF_OpenFont(FONT_CANDIDATES[i], 13); fonts->medium = TTF_OpenFont(FONT_CANDIDATES[i], 17); diff --git a/src/ui.d b/src/ui.d new file mode 100644 index 0000000..f011b0c --- /dev/null +++ b/src/ui.d @@ -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: