feat: Background as video support
This commit is contained in:
22
Makefile
22
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)
|
||||
|
||||
2
PKGBUILD
2
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
|
||||
|
||||
16
README.md
16
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 <image_path> <game_executable> [game_arguments...]
|
||||
gsplash <image_or_video_path> <game_executable> [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] <image> <executable> [args...]
|
||||
build/gsplash [--mode=stretch|center|crop] <background> <executable> [args...]
|
||||
|
||||
gsplash -m stretch|center|crop <image> <executable> [args...]
|
||||
build/gsplash -m stretch|center|crop <background> <executable> [args...]
|
||||
```
|
||||
358
src/gsplash.c
358
src/gsplash.c
@@ -8,6 +8,13 @@
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libswscale/swscale.h>
|
||||
#include <libavutil/imgutils.h>
|
||||
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user