diff --git a/Makefile b/Makefile index f1719fb..52d29a2 100644 --- a/Makefile +++ b/Makefile @@ -9,32 +9,38 @@ PKG_CONFIG ?= pkg-config SDL_CFLAGS := $(shell $(PKG_CONFIG) --cflags sdl2 SDL2_image) SDL_LIBS := $(shell $(PKG_CONFIG) --libs sdl2 SDL2_image) +FFMPEG_CFLAGS := $(shell $(PKG_CONFIG) --cflags libavformat libavcodec libswscale libavutil) +FFMPEG_LIBS := $(shell $(PKG_CONFIG) --libs libavformat libavcodec libswscale libavutil) -ALL_CFLAGS := $(CFLAGS) $(SDL_CFLAGS) -ALL_LIBS := $(LIBS) $(SDL_LIBS) -lm +ALL_CFLAGS := $(CFLAGS) $(SDL_CFLAGS) $(FFMPEG_CFLAGS) +ALL_LIBS := $(LIBS) $(SDL_LIBS) $(FFMPEG_LIBS) -lm -TARGET = gsplash +BUILD_DIR ?= build +TARGET = $(BUILD_DIR)/gsplash SRC = src/gsplash.c .PHONY: all clean install uninstall check all: $(TARGET) -$(TARGET): $(SRC) - $(CC) $(ALL_CFLAGS) $(LDFLAGS) $< -o $@ $(ALL_LIBS) +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +$(TARGET): $(BUILD_DIR) $(SRC) + $(CC) $(ALL_CFLAGS) $(LDFLAGS) $(SRC) -o $@ $(ALL_LIBS) # Lightweight smoke test (headless via SDL_VIDEODRIVER=dummy) check: $(TARGET) @echo "Running smoke test (headless)..." - SDL_VIDEODRIVER=dummy ./$(TARGET) nonexistent.png /bin/true || true + SDL_VIDEODRIVER=dummy $(TARGET) nonexistent.png /bin/true || true @echo "Smoke test finished" install: $(TARGET) install -d "$(DESTDIR)$(bindir)" - install -m 755 $(TARGET) "$(DESTDIR)$(bindir)/$(TARGET)" + install -m 755 $(TARGET) "$(DESTDIR)$(bindir)/gsplash" uninstall: - rm -f "$(DESTDIR)$(bindir)/$(TARGET)" + rm -f "$(DESTDIR)$(bindir)/gsplash" clean: rm -f $(TARGET) diff --git a/PKGBUILD b/PKGBUILD index 75c291a..b393adb 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -4,7 +4,7 @@ pkgrel=1 pkgdesc="A lightweight native SDL2 splash screen wrapper for game launchers." arch=('x86_64') license=('custom') -depends=('sdl2' 'sdl2_image') +depends=('sdl2' 'sdl2_image' 'ffmpeg') makedepends=('make' 'gcc' 'pkgconf') # Leave this empty so makepkg doesn't look for external downloads or local copies diff --git a/README.md b/README.md index 7f05a83..83627a2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A fullscreen splash-screen wrapper for launching a game or app. It displays an i ## Features 1. Fullscreen, borderless splash screen with hidden cursor (SDL2) -2. Displays a supplied image via SDL2_image (fallback to black if load fails) +2. Displays a supplied image via SDL2_image or video via ffmpeg (fallback to black if load fails) 3. Launches the target executable and exits when it finishes 4. Hides on focus loss after the game starts or closes on **Esc** @@ -16,6 +16,7 @@ A fullscreen splash-screen wrapper for launching a game or app. It displays an i - pkg-config - SDL2 - SDL2_image +- ffmpeg (libavformat, libavcodec, libswscale, libavutil) ## Install @@ -28,7 +29,7 @@ makepkg -si For other distributions, build and install manually: ```bash -# Build the binary in the project root +# Build the binary into build/ make # Install system-wide (defaults to /usr/local) @@ -49,16 +50,19 @@ make check ## Usage ```bash -gsplash [game_arguments...] +gsplash [game_arguments...] ``` Example: ```bash gsplash assets/splash.jpg /path/to/game --fullscreen --profile=default + +# Video splash (supported formats depend on ffmpeg build) +gsplash assets/splash.mp4 /path/to/game --fullscreen --profile=default ``` -Gsplash allows you to configure how the image is displayed with 3 modes: +Gsplash allows you to configure how the image or video is displayed with 3 modes: - `center` (default): letterbox - `crop`: fill screen by cropping @@ -67,7 +71,7 @@ Gsplash allows you to configure how the image is displayed with 3 modes: You can set these by using the `-m` or `--mode` flag: ```bash -gsplash [--mode=stretch|center|crop] [args...] +build/gsplash [--mode=stretch|center|crop] [args...] -gsplash -m stretch|center|crop [args...] +build/gsplash -m stretch|center|crop [args...] ``` \ No newline at end of file diff --git a/src/gsplash.c b/src/gsplash.c index dbb9f31..a795343 100644 --- a/src/gsplash.c +++ b/src/gsplash.c @@ -8,6 +8,13 @@ #include #include #include +#include +#include + +#include +#include +#include +#include static void log_info(const char *fmt, ...) { @@ -51,6 +58,255 @@ static RenderMode parse_render_mode(const char *value) return RENDER_STRETCH; } +typedef struct VideoPlayer +{ + AVFormatContext *format_ctx; + AVCodecContext *codec_ctx; + struct SwsContext *sws_ctx; + AVFrame *frame; + AVFrame *rgba_frame; + uint8_t *rgba_buffer; + int rgba_buffer_size; + int stream_index; + int width; + int height; + int frame_delay_ms; + Uint32 next_frame_tick; + SDL_Texture *texture; +} VideoPlayer; + +static bool has_video_extension(const char *path) +{ + const char *dot = strrchr(path, '.'); + if (!dot || dot == path) + { + return false; + } + + char ext[8]; + size_t i = 0; + dot++; + while (*dot && i < sizeof(ext) - 1) + { + ext[i++] = (char)tolower((unsigned char)*dot++); + } + ext[i] = '\0'; + + return strcmp(ext, "mp4") == 0 || strcmp(ext, "mkv") == 0 || strcmp(ext, "webm") == 0 || + strcmp(ext, "avi") == 0 || strcmp(ext, "mov") == 0 || strcmp(ext, "mpg") == 0 || + strcmp(ext, "mpeg") == 0; +} + +static void compute_dest_rect(int src_w, int src_h, int out_w, int out_h, RenderMode mode, SDL_Rect *dst) +{ + dst->x = 0; + dst->y = 0; + dst->w = out_w; + dst->h = out_h; + + if (src_w <= 0 || src_h <= 0 || out_w <= 0 || out_h <= 0) + { + return; + } + + if (mode == RENDER_CENTER) + { + float scale = fminf((float)out_w / (float)src_w, (float)out_h / (float)src_h); + dst->w = (int)(src_w * scale); + dst->h = (int)(src_h * scale); + dst->x = (out_w - dst->w) / 2; + dst->y = (out_h - dst->h) / 2; + } + else if (mode == RENDER_CROP) + { + float scale = fmaxf((float)out_w / (float)src_w, (float)out_h / (float)src_h); + dst->w = (int)(src_w * scale); + dst->h = (int)(src_h * scale); + dst->x = (out_w - dst->w) / 2; + dst->y = (out_h - dst->h) / 2; + } +} + +static bool init_video_player(VideoPlayer *player, SDL_Renderer *renderer, const char *path) +{ + memset(player, 0, sizeof(*player)); + + if (avformat_open_input(&player->format_ctx, path, NULL, NULL) != 0) + { + return false; + } + + if (avformat_find_stream_info(player->format_ctx, NULL) < 0) + { + return false; + } + + player->stream_index = -1; + for (unsigned int i = 0; i < player->format_ctx->nb_streams; ++i) + { + if (player->format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) + { + player->stream_index = (int)i; + break; + } + } + + if (player->stream_index < 0) + { + return false; + } + + AVCodecParameters *codecpar = player->format_ctx->streams[player->stream_index]->codecpar; + const AVCodec *decoder = avcodec_find_decoder(codecpar->codec_id); + if (!decoder) + { + return false; + } + + player->codec_ctx = avcodec_alloc_context3(decoder); + if (!player->codec_ctx) + { + return false; + } + + if (avcodec_parameters_to_context(player->codec_ctx, codecpar) < 0) + { + return false; + } + + if (avcodec_open2(player->codec_ctx, decoder, NULL) < 0) + { + return false; + } + + player->width = player->codec_ctx->width; + player->height = player->codec_ctx->height; + player->frame = av_frame_alloc(); + player->rgba_frame = av_frame_alloc(); + if (!player->frame || !player->rgba_frame) + { + return false; + } + + player->rgba_buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGBA, player->width, player->height, 1); + player->rgba_buffer = (uint8_t *)av_malloc(player->rgba_buffer_size); + if (!player->rgba_buffer) + { + return false; + } + + av_image_fill_arrays(player->rgba_frame->data, player->rgba_frame->linesize, player->rgba_buffer, + AV_PIX_FMT_RGBA, player->width, player->height, 1); + + player->sws_ctx = sws_getContext(player->width, player->height, player->codec_ctx->pix_fmt, + player->width, player->height, AV_PIX_FMT_RGBA, SWS_BILINEAR, + NULL, NULL, NULL); + if (!player->sws_ctx) + { + return false; + } + + player->texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, + SDL_TEXTUREACCESS_STREAMING, player->width, player->height); + if (!player->texture) + { + return false; + } + + AVRational fr = player->format_ctx->streams[player->stream_index]->avg_frame_rate; + if (fr.num > 0 && fr.den > 0) + { + player->frame_delay_ms = (int)(1000.0 * fr.den / fr.num); + } + else + { + player->frame_delay_ms = 33; + } + + player->next_frame_tick = SDL_GetTicks(); + return true; +} + +static void cleanup_video_player(VideoPlayer *player) +{ + if (player->texture) + { + SDL_DestroyTexture(player->texture); + } + if (player->sws_ctx) + { + sws_freeContext(player->sws_ctx); + } + if (player->rgba_buffer) + { + av_free(player->rgba_buffer); + } + if (player->rgba_frame) + { + av_frame_free(&player->rgba_frame); + } + if (player->frame) + { + av_frame_free(&player->frame); + } + if (player->codec_ctx) + { + avcodec_free_context(&player->codec_ctx); + } + if (player->format_ctx) + { + avformat_close_input(&player->format_ctx); + } + memset(player, 0, sizeof(*player)); +} + +static bool decode_next_frame(VideoPlayer *player) +{ + AVPacket packet; + av_init_packet(&packet); + + while (av_read_frame(player->format_ctx, &packet) >= 0) + { + if (packet.stream_index != player->stream_index) + { + av_packet_unref(&packet); + continue; + } + + if (avcodec_send_packet(player->codec_ctx, &packet) < 0) + { + av_packet_unref(&packet); + continue; + } + av_packet_unref(&packet); + + while (true) + { + int ret = avcodec_receive_frame(player->codec_ctx, player->frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) + { + break; + } + if (ret < 0) + { + return false; + } + + sws_scale(player->sws_ctx, (const uint8_t *const *)player->frame->data, + player->frame->linesize, 0, player->height, + player->rgba_frame->data, player->rgba_frame->linesize); + + SDL_UpdateTexture(player->texture, NULL, player->rgba_frame->data[0], + player->rgba_frame->linesize[0]); + return true; + } + } + + av_seek_frame(player->format_ctx, player->stream_index, 0, AVSEEK_FLAG_BACKWARD); + avcodec_flush_buffers(player->codec_ctx); + return false; +} + int main(int argc, char *argv[]) { RenderMode render_mode = RENDER_CENTER; @@ -120,9 +376,31 @@ int main(int argc, char *argv[]) { log_info("Renderer created (accelerated)"); } - SDL_Texture *texture = IMG_LoadTexture(renderer, image_path); + SDL_Texture *texture = NULL; + VideoPlayer video_player; + bool video_active = false; - if (!texture) + if (has_video_extension(image_path)) + { + if (init_video_player(&video_player, renderer, image_path)) + { + video_active = true; + } + else + { + log_error("Failed to open video '%s'; showing black screen", image_path); + } + } + else + { + texture = IMG_LoadTexture(renderer, image_path); + if (!texture && init_video_player(&video_player, renderer, image_path)) + { + video_active = true; + } + } + + if (!texture && !video_active) { log_error("Failed to load splash image '%s': %s; showing black screen", image_path, IMG_GetError()); // Fallback: Clear to solid black if image file is broken @@ -132,51 +410,43 @@ int main(int argc, char *argv[]) } else { - log_info("Splash image loaded"); - SDL_RenderClear(renderer); - SDL_Rect dst_rect = {0, 0, 0, 0}; - int tex_w = 0; - int tex_h = 0; int out_w = 0; int out_h = 0; - if (SDL_QueryTexture(texture, NULL, NULL, &tex_w, &tex_h) == 0 && - SDL_GetRendererOutputSize(renderer, &out_w, &out_h) == 0 && - tex_w > 0 && tex_h > 0 && out_w > 0 && out_h > 0) + SDL_GetRendererOutputSize(renderer, &out_w, &out_h); + + if (video_active) { - if (render_mode == RENDER_CENTER) + log_info("Video splash loaded"); + compute_dest_rect(video_player.width, video_player.height, out_w, out_h, render_mode, &dst_rect); + decode_next_frame(&video_player); + SDL_RenderClear(renderer); + SDL_RenderCopy(renderer, video_player.texture, NULL, &dst_rect); + SDL_RenderPresent(renderer); + } + else if (texture) + { + int tex_w = 0; + int tex_h = 0; + + log_info("Splash image loaded"); + if (SDL_QueryTexture(texture, NULL, NULL, &tex_w, &tex_h) == 0) { - float scale = fminf((float)out_w / (float)tex_w, (float)out_h / (float)tex_h); - dst_rect.w = (int)(tex_w * scale); - dst_rect.h = (int)(tex_h * scale); - dst_rect.x = (out_w - dst_rect.w) / 2; - dst_rect.y = (out_h - dst_rect.h) / 2; + compute_dest_rect(tex_w, tex_h, out_w, out_h, render_mode, &dst_rect); } - else if (render_mode == RENDER_CROP) + + SDL_RenderClear(renderer); + if (dst_rect.w > 0 && dst_rect.h > 0) { - float scale = fmaxf((float)out_w / (float)tex_w, (float)out_h / (float)tex_h); - dst_rect.w = (int)(tex_w * scale); - dst_rect.h = (int)(tex_h * scale); - dst_rect.x = (out_w - dst_rect.w) / 2; - dst_rect.y = (out_h - dst_rect.h) / 2; + SDL_RenderCopy(renderer, texture, NULL, &dst_rect); } else { - dst_rect.w = out_w; - dst_rect.h = out_h; + SDL_RenderCopy(renderer, texture, NULL, NULL); } + SDL_RenderPresent(renderer); } - - if (dst_rect.w > 0 && dst_rect.h > 0) - { - SDL_RenderCopy(renderer, texture, NULL, &dst_rect); - } - else - { - SDL_RenderCopy(renderer, texture, NULL, NULL); - } - SDL_RenderPresent(renderer); } // Fork the process to run the game @@ -228,6 +498,24 @@ int main(int argc, char *argv[]) break; // Process error safetynet } + if (video_active && SDL_GetTicks() >= video_player.next_frame_tick) + { + if (!decode_next_frame(&video_player)) + { + // If decoding fails or loops, try again on next tick + } + video_player.next_frame_tick = SDL_GetTicks() + (Uint32)video_player.frame_delay_ms; + + SDL_RenderClear(renderer); + SDL_Rect dst_rect = {0, 0, 0, 0}; + int out_w = 0; + int out_h = 0; + SDL_GetRendererOutputSize(renderer, &out_w, &out_h); + compute_dest_rect(video_player.width, video_player.height, out_w, out_h, render_mode, &dst_rect); + SDL_RenderCopy(renderer, video_player.texture, NULL, &dst_rect); + SDL_RenderPresent(renderer); + } + // Check desktop server window events while (SDL_PollEvent(&event)) { @@ -261,6 +549,8 @@ int main(int argc, char *argv[]) // Clean up memory space if (texture) SDL_DestroyTexture(texture); + if (video_active) + cleanup_video_player(&video_player); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); IMG_Quit();