From d389fbd6d5a002d24a35c89e37a6e0e71981afa0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Hamal=20Dvo=C5=99=C3=A1k?= <mordae@anilinux.org>
Date: Sun, 16 Mar 2025 19:00:26 +0100
Subject: [PATCH] Initial import

---
 .clang-format      | 112 +++++++++++++++++++
 .clangd            |   2 +
 .gitignore         |   1 +
 src/CMakeLists.txt |  25 +++++
 src/main.c         | 266 +++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 406 insertions(+)
 create mode 100644 .clang-format
 create mode 100644 .clangd
 create mode 100644 .gitignore
 create mode 100644 src/CMakeLists.txt
 create mode 100644 src/main.c

diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..54a2757
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,112 @@
+---
+AccessModifierOffset: -4
+AlignAfterOpenBracket: Align
+AlignConsecutiveAssignments: false
+AlignConsecutiveDeclarations: false
+AlignEscapedNewlines: Left
+AlignOperands: true
+AlignTrailingComments: true
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: false
+BinPackArguments: true
+BinPackParameters: true
+BraceWrapping:
+  AfterClass: false
+  AfterControlStatement: false
+  AfterEnum: false
+  AfterFunction: true
+  AfterNamespace: true
+  AfterObjCDeclaration: false
+  AfterStruct: false
+  AfterUnion: false
+  AfterExternBlock: false
+  BeforeCatch: false
+  BeforeElse: false
+  IndentBraces: false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: None
+BreakBeforeBraces: Custom
+BreakBeforeInheritanceComma: false
+BreakBeforeTernaryOperators: false
+BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: BeforeComma
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: false
+ColumnLimit: 100
+CommentPragmas: '^ IWYU pragma:'
+CompactNamespaces: false
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: false
+DerivePointerAlignment: false
+DisableFormat: false
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: false
+
+ForEachMacros:
+  - 'for_each'
+
+IncludeBlocks: Preserve
+IncludeCategories:
+  - Regex: '.*'
+    Priority: 1
+IncludeIsMainRegex: '(Test)?$'
+IndentCaseLabels: false
+IndentGotoLabels: false
+IndentPPDirectives: None
+IndentWidth: 4
+IndentWrappedFunctionNames: false
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: false
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 4
+ObjCSpaceAfterProperty: true
+ObjCSpaceBeforeProtocolList: true
+
+# Taken from git's rules
+PenaltyBreakAssignment: 10
+PenaltyBreakBeforeFirstCallParameter: 30
+PenaltyBreakComment: 10
+PenaltyBreakFirstLessLess: 0
+PenaltyBreakString: 10
+PenaltyExcessCharacter: 100
+PenaltyReturnTypeOnItsOwnLine: 60
+
+PointerAlignment: Right
+ReflowComments: false
+SortIncludes: false
+SortUsingDeclarations: false
+SpaceAfterCStyleCast: false
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: ControlStatementsExceptForEachMacros
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles: false
+SpacesInContainerLiterals: false
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+Standard: Cpp03
+TabWidth: 4
+UseTab: Always
+...
diff --git a/.clangd b/.clangd
new file mode 100644
index 0000000..ec0b890
--- /dev/null
+++ b/.clangd
@@ -0,0 +1,2 @@
+CompileFlags:
+  CompilationDatabase: build
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..84c7387
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,25 @@
+cmake_minimum_required(VERSION 3.21)
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+project(lazier-player)
+
+find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
+
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(mpv REQUIRED IMPORTED_TARGET mpv)
+
+if(CMAKE_BUILD_TYPE MATCHES "Debug")
+  add_compile_options(-fsanitize=address)
+  add_link_options(-fsanitize=address)
+endif()
+
+add_executable(
+  lazier-player
+  main.c
+)
+
+target_link_libraries(lazier-player PRIVATE SDL3::SDL3 mpv m)
+set_property(TARGET lazier-player PROPERTY C_STANDARD 23)
+target_compile_options(lazier-player PRIVATE -Wall -Wextra -Wnull-dereference)
+target_include_directories(lazier-player PRIVATE include)
+install(TARGETS lazier-player DESTINATION bin)
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..4719db1
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,266 @@
+#include <SDL3/SDL.h>
+#include <mpv/client.h>
+#include <mpv/render.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+typedef struct {
+	mpv_handle *mpv;
+	mpv_render_context *render_ctx;
+	SDL_Window *window;
+	SDL_Renderer *renderer;
+	SDL_Texture *texture;
+	int width;
+	int height;
+	int running;
+	int needs_redraw;
+} App;
+
+static void SDL_PrintError(const char *msg)
+{
+	fprintf(stderr, "sdl: %s: %s\n", msg, SDL_GetError());
+}
+
+static void mpv_print_error(int code, const char *msg)
+{
+	fprintf(stderr, "mpv: %s: %s\n", msg, mpv_error_string(code));
+}
+
+static void handle_mpv_events(App *app)
+{
+	while (true) {
+		mpv_event *event = mpv_wait_event(app->mpv, 0);
+
+		if (event->event_id == MPV_EVENT_NONE)
+			break;
+
+		printf("mpv: event=%s\n", mpv_event_name(event->event_id));
+
+		if (event->event_id == MPV_EVENT_PROPERTY_CHANGE) {
+			mpv_event_property *prop = event->data;
+
+			if (!strcmp(prop->name, "width") || !strcmp(prop->name, "height")) {
+				int64_t width = 0;
+				int64_t height = 0;
+
+				mpv_get_property(app->mpv, "width", MPV_FORMAT_INT64, &width);
+				mpv_get_property(app->mpv, "height", MPV_FORMAT_INT64, &height);
+
+				if (width > 0 && height > 0 && (width != app->width || height != app->height)) {
+					app->width = width;
+					app->height = height;
+
+					if (app->texture)
+						SDL_DestroyTexture(app->texture);
+
+					app->texture = SDL_CreateTexture(app->renderer, SDL_PIXELFORMAT_XBGR8888,
+													 SDL_TEXTUREACCESS_STREAMING, width, height);
+
+					if (!app->texture) {
+						SDL_PrintError("SDL_CreateTexture");
+					} else {
+						// Resize window to match video dimensions
+						SDL_SetWindowSize(app->window, app->width, app->height);
+					}
+				}
+			}
+		} else if (event->event_id == MPV_EVENT_END_FILE) {
+			app->running = 0;
+		}
+	}
+}
+
+static void render_video(App *app)
+{
+	int err;
+
+	// Clear the renderer
+	if (!SDL_SetRenderDrawColor(app->renderer, 0, 0, 0, 255))
+		SDL_PrintError("SDL_SetRenderDrawColor");
+
+	if (!SDL_RenderClear(app->renderer))
+		SDL_PrintError("SDL_RenderClear");
+
+	// Render MPV frame
+	if (app->texture && app->render_ctx) {
+		void *pixels;
+		int stride = 0;
+
+		if (!SDL_LockTexture(app->texture, NULL, &pixels, &stride)) {
+			SDL_PrintError("SDL_LockTexture");
+			return;
+		}
+
+		float w, h;
+		if (!SDL_GetTextureSize(app->texture, &w, &h))
+			SDL_PrintError("SDL_GetTextureSize");
+
+		int size[] = { w, h };
+
+		mpv_render_param params[] = {
+			{ MPV_RENDER_PARAM_SW_SIZE, &size },	 //
+			{ MPV_RENDER_PARAM_SW_FORMAT, "rgb0" },	 //
+			{ MPV_RENDER_PARAM_SW_STRIDE, &stride }, //
+			{ MPV_RENDER_PARAM_SW_POINTER, pixels }, //
+			{ MPV_RENDER_PARAM_INVALID, NULL },
+		};
+
+		if ((err = mpv_render_context_render(app->render_ctx, params)))
+			mpv_print_error(err, "mpv_render_context_render");
+
+		SDL_UnlockTexture(app->texture);
+
+		if (!SDL_RenderTexture(app->renderer, app->texture, NULL, NULL)) {
+			SDL_PrintError("SDL_RenderTexture");
+		}
+	}
+
+	// Draw shapes over the video
+	// 1. Red rectangle
+	if (!SDL_SetRenderDrawColor(app->renderer, 255, 0, 0, 128))
+		SDL_PrintError("SDL_SetRenderDrawColor");
+
+	SDL_FRect rect = { 50, 50, 100, 80 };
+
+	if (!SDL_RenderFillRect(app->renderer, &rect))
+		SDL_PrintError("SDL_RenderFillRect");
+
+	// 2. Blue circle (approximated with points)
+	if (!SDL_SetRenderDrawColor(app->renderer, 0, 0, 255, 128))
+		SDL_PrintError("SDL_SetRenderDrawColor");
+
+	int centerX = app->width - 100;
+	int centerY = app->height - 100;
+	int radius = 40;
+
+	for (int i = 0; i < 360; i++) {
+		float angle = i * 3.14159f / 180.0f;
+		float x = centerX + radius * SDL_cosf(angle);
+		float y = centerY + radius * SDL_sinf(angle);
+
+		if (!SDL_RenderPoint(app->renderer, x, y))
+			SDL_PrintError("SDL_RenderPoint");
+	}
+
+	// 3. Green line
+	if (!SDL_SetRenderDrawColor(app->renderer, 0, 255, 0, 200))
+		SDL_PrintError("SDL_SetRenderDrawColor");
+
+	if (!SDL_RenderLine(app->renderer, 0, 0, app->width, app->height))
+		SDL_PrintError("SDL_RenderLine");
+
+	if (!SDL_RenderPresent(app->renderer))
+		SDL_PrintError("SDL_RenderPresent");
+}
+
+int main(int argc, char *argv[])
+{
+	int err;
+
+	if (argc < 2) {
+		printf("Usage: %s <video_file>\n", argv[0]);
+		return 1;
+	}
+
+	static App app = { 0 };
+	app.running = 1;
+	app.width = 800;
+	app.height = 600;
+
+	// Initialize SDL
+	if (!SDL_Init(SDL_INIT_VIDEO)) {
+		SDL_PrintError("SDL_Init");
+		return 1;
+	}
+
+	// Create window
+	app.window = SDL_CreateWindow("SDL3 + MPV Video Player", app.width, app.height, 0);
+	if (!app.window) {
+		SDL_PrintError("SDL_CreateWindow");
+		SDL_Quit();
+		return 1;
+	}
+
+	// Create renderer
+	app.renderer = SDL_CreateRenderer(app.window, "software");
+	if (!app.renderer) {
+		SDL_PrintError("SDL_CreateRenderer");
+		SDL_DestroyWindow(app.window);
+		SDL_Quit();
+		return 1;
+	}
+
+	// Initialize MPV
+	app.mpv = mpv_create();
+	if (!app.mpv) {
+		fprintf(stderr, "mpv: mpv_create: returned NULL\n");
+		SDL_DestroyRenderer(app.renderer);
+		SDL_DestroyWindow(app.window);
+		SDL_Quit();
+		return 1;
+	}
+
+	// Configure MPV
+	if ((err = mpv_set_option_string(app.mpv, "video-sync", "display-resample")))
+		mpv_print_error(err, "mpv_set_option_string");
+
+	if ((err = mpv_set_option_string(app.mpv, "vo", "libmpv")))
+		mpv_print_error(err, "mpv_set_option_string");
+
+	if ((err = mpv_initialize(app.mpv)))
+		mpv_print_error(err, "mpv_initialize");
+
+	// Initialize MPV render context
+	mpv_render_param params[] = {
+		{ MPV_RENDER_PARAM_API_TYPE, MPV_RENDER_API_TYPE_SW },
+		{ MPV_RENDER_PARAM_INVALID, NULL },
+	};
+
+	if ((err = mpv_render_context_create(&app.render_ctx, app.mpv, params)))
+		mpv_print_error(err, "mpv_render_context_create");
+
+	// Load the video file
+	const char *cmd[] = { "loadfile", argv[1], NULL };
+
+	if ((err = mpv_command(app.mpv, cmd)))
+		mpv_print_error(err, "mpv_command");
+
+	// Observe properties
+	if ((err = mpv_observe_property(app.mpv, 0, "width", MPV_FORMAT_INT64)))
+		mpv_print_error(err, "mpv_observe_property");
+
+	if ((err = mpv_observe_property(app.mpv, 0, "height", MPV_FORMAT_INT64)))
+		mpv_print_error(err, "mpv_observe_property");
+
+	// Main loop
+	while (app.running) {
+		SDL_Event event;
+		while (SDL_PollEvent(&event)) {
+			if (event.type == SDL_EVENT_QUIT) {
+				app.running = 0;
+			}
+		}
+
+		handle_mpv_events(&app);
+		render_video(&app);
+
+		SDL_Delay(16); // ~60fps
+	}
+
+	// Cleanup
+	if (app.texture)
+		SDL_DestroyTexture(app.texture);
+
+	if (app.render_ctx)
+		mpv_render_context_free(app.render_ctx);
+
+	mpv_terminate_destroy(app.mpv);
+	SDL_DestroyRenderer(app.renderer);
+	SDL_DestroyWindow(app.window);
+	SDL_Quit();
+
+	return 0;
+}
+
+// vim:set ts=4 sw=4 et: