فهرست منبع

feat: video texture support (#478)

* feat: support video textures

* feat: basic support for parent/child relationships in objects

* chore: formatting

* chore: update textures when used, not all the time

* fix: video texture looping

* fix: choppy playback on video textures was caused by audio synchronization, audio is already muted, so untimed playback is enabled

* chore: cleanup video-playing code and removed duplication

* chore: cleanup video-playing code and provide a mechanism to stop videos not in use anymore (useful for playlists)

* chore: apply formatting

* chore: ensure video textures have one mipmap & properly free resources

* chore: do not initialize video on render, fix texture format and detect memory stream protocol registration failures

* chore: use mpv_set_property and mpv_set_property_string for changes after mpv is initialized

* chore: prevent untimed setting from being changed while playing (not supported by mpv) and set pause properly

fixes #475
Alexis Maiquez 7 ماه پیش
والد
کامیت
1615863d84
27فایلهای تغییر یافته به همراه713 افزوده شده و 200 حذف شده
  1. 6 1
      CMakeLists.txt
  2. 18 16
      src/WallpaperEngine/Application/WallpaperApplication.cpp
  3. 3 3
      src/WallpaperEngine/Data/Model/Object.h
  4. 8 7
      src/WallpaperEngine/Data/Parsers/ObjectParser.cpp
  5. 2 0
      src/WallpaperEngine/Data/Utils/MemoryStream.h
  6. 5 1
      src/WallpaperEngine/Render/CFBO.cpp
  7. 4 0
      src/WallpaperEngine/Render/CFBO.h
  8. 5 1
      src/WallpaperEngine/Render/CObject.cpp
  9. 5 6
      src/WallpaperEngine/Render/CObject.h
  10. 57 10
      src/WallpaperEngine/Render/CTexture.cpp
  11. 31 4
      src/WallpaperEngine/Render/CTexture.h
  12. 0 3
      src/WallpaperEngine/Render/CWallpaper.cpp
  13. 2 1
      src/WallpaperEngine/Render/CWallpaper.h
  14. 1 1
      src/WallpaperEngine/Render/Drivers/GLFWOpenGLDriver.cpp
  15. 1 0
      src/WallpaperEngine/Render/Drivers/VideoFactories.h
  16. 13 0
      src/WallpaperEngine/Render/Objects/CImage.cpp
  17. 1 0
      src/WallpaperEngine/Render/RenderContext.cpp
  18. 2 3
      src/WallpaperEngine/Render/TextureCache.cpp
  19. 19 0
      src/WallpaperEngine/Render/TextureProvider.h
  20. 43 0
      src/WallpaperEngine/Render/Wallpapers/CScene.cpp
  21. 1 0
      src/WallpaperEngine/Render/Wallpapers/CScene.h
  22. 26 134
      src/WallpaperEngine/Render/Wallpapers/CVideo.cpp
  23. 5 9
      src/WallpaperEngine/Render/Wallpapers/CVideo.h
  24. 296 0
      src/WallpaperEngine/VideoPlayback/MPV/GLPlayer.cpp
  25. 81 0
      src/WallpaperEngine/VideoPlayback/MPV/GLPlayer.h
  26. 54 0
      src/WallpaperEngine/VideoPlayback/MPV/MemoryStreamProtocol.cpp
  27. 24 0
      src/WallpaperEngine/VideoPlayback/MPV/MemoryStreamProtocol.h

+ 6 - 1
CMakeLists.txt

@@ -462,7 +462,12 @@ set(COMMON_SOURCES
     src/WallpaperEngine/Data/Builders/VectorBuilder.h
     src/WallpaperEngine/Data/Dumpers/StringPrinter.cpp
     src/WallpaperEngine/Data/Dumpers/StringPrinter.h
-    src/WallpaperEngine/Data/JSON.cpp)
+    src/WallpaperEngine/Data/JSON.cpp
+
+    src/WallpaperEngine/VideoPlayback/MPV/GLPlayer.cpp
+    src/WallpaperEngine/VideoPlayback/MPV/GLPlayer.h
+    src/WallpaperEngine/VideoPlayback/MPV/MemoryStreamProtocol.cpp
+    src/WallpaperEngine/VideoPlayback/MPV/MemoryStreamProtocol.h)
 
 if(BUILD_TESTING)
     add_executable(

+ 18 - 16
src/WallpaperEngine/Application/WallpaperApplication.cpp

@@ -199,13 +199,13 @@ ProjectUniquePtr WallpaperApplication::loadBackground (const std::string& bg) {
     // this allows taking screenshots after a background changes
     // useful for playlists
     if (this->m_context.settings.screenshot.take) {
-        this->m_nextFrameScreenshot = this->m_context.settings.screenshot.delay;
+	this->m_nextFrameScreenshot = this->m_context.settings.screenshot.delay;
 
-        if (this->m_videoDriver != nullptr) {
-            this->m_nextFrameScreenshot += this->m_videoDriver->getFrameCounter ();
-        }
+	if (this->m_videoDriver != nullptr) {
+	    this->m_nextFrameScreenshot += this->m_videoDriver->getFrameCounter ();
+	}
 
-        this->m_screenShotTaken = false;
+	this->m_screenShotTaken = false;
     }
 
     return WallpaperEngine::Data::Parsers::ProjectParser::parse (json, std::move (container));
@@ -587,7 +587,7 @@ void WallpaperApplication::takeScreenshot (const std::filesystem::path& filename
     const std::string extStr = extension.string ();
 
     // Offload pixel processing and saving to a background thread to avoid hitches
-    std::thread ([captures, width, height, vflip, extStr, filename]() {
+    std::thread ([captures, width, height, vflip, extStr, filename] () {
 	auto* bitmap = new uint8_t[width * height * 3] { 0 };
 
 	for (const auto& capture : captures) {
@@ -595,8 +595,10 @@ void WallpaperApplication::takeScreenshot (const std::filesystem::path& filename
 	    for (int y = 0; y < capture.vpHeight; y++) {
 		for (int x = 0; x < capture.vpWidth; x++) {
 		    // interpolate within the UV range to get source coordinates
-		    const float u = capture.ustart + (static_cast<float> (x) / capture.vpWidth) * (capture.uend - capture.ustart);
-		    const float v = capture.vstart + (static_cast<float> (y) / capture.vpHeight) * (capture.vend - capture.vstart);
+		    const float u
+			= capture.ustart + (static_cast<float> (x) / capture.vpWidth) * (capture.uend - capture.ustart);
+		    const float v = capture.vstart
+			+ (static_cast<float> (y) / capture.vpHeight) * (capture.vend - capture.vstart);
 
 		    // convert UV to pixel coordinates in the source buffer
 		    const int srcX = std::clamp (static_cast<int> (u * capture.readWidth), 0, capture.readWidth - 1);
@@ -826,16 +828,16 @@ void WallpaperApplication::show () {
 
 	this->updatePlaylists ();
 
-        if (!this->m_context.settings.screenshot.take || this->m_screenShotTaken == true) {
-            continue;
-        }
+	if (!this->m_context.settings.screenshot.take || this->m_screenShotTaken == true) {
+	    continue;
+	}
 
-        if (this->m_videoDriver->getFrameCounter () < this->m_nextFrameScreenshot) {
-            continue;
-        }
+	if (this->m_videoDriver->getFrameCounter () < this->m_nextFrameScreenshot) {
+	    continue;
+	}
 
-        this->takeScreenshot (this->m_context.settings.screenshot.path);
-        this->m_screenShotTaken = true;
+	this->takeScreenshot (this->m_context.settings.screenshot.path);
+	this->m_screenShotTaken = true;
     }
 
     sLog.out ("Stopping");

+ 3 - 3
src/WallpaperEngine/Data/Model/Object.h

@@ -22,6 +22,9 @@ struct ObjectData {
     int id;
     std::string name;
     std::vector<int> dependencies;
+    std::optional<int> parent;
+    /** The point of origin of the object */
+    UserSettingUniquePtr origin;
 };
 
 /**
@@ -86,8 +89,6 @@ struct ImageAnimationLayer {
 };
 
 struct ImageData {
-    /** The point of origin of the image */
-    UserSettingUniquePtr origin;
     /** The scale of the image */
     UserSettingUniquePtr scale;
     /** The rotation of the image */
@@ -537,7 +538,6 @@ struct ParticleInstanceOverride {
 
 struct ParticleData {
     /** Position and transformation */
-    UserSettingUniquePtr origin;
     UserSettingUniquePtr scale;
     UserSettingUniquePtr angles;
     UserSettingUniquePtr visible;

+ 8 - 7
src/WallpaperEngine/Data/Parsers/ObjectParser.cpp

@@ -29,6 +29,8 @@ ObjectUniquePtr ObjectParser::parse (const JSON& it, const Project& project) {
 	    .id = it.require<int> ("id", "Object must have an id"),
 	    .name = it.require<std::string> ("name", "Object must have a name"),
 	    .dependencies = parseDependencies (it),
+	    .parent = it.optional<int> ("parent"),
+	    .origin = it.user ("origin", project.properties, glm::vec3 (0.0f)),
 	};
     } catch (const std::exception& e) {
 	sLog.error ("Error parsing object base data: ", e.what ());
@@ -47,21 +49,23 @@ ObjectUniquePtr ObjectParser::parse (const JSON& it, const Project& project) {
     }
 
     if (imageIt != it.end () && imageIt->is_string ()) {
-	return parseImage (it, project, basedata, *imageIt);
+	return parseImage (it, project, std::move (basedata), *imageIt);
     } else if (soundIt != it.end () && soundIt->is_array ()) {
-	return parseSound (it, basedata);
+	return parseSound (it, std::move (basedata));
     } else if (particleIt != it.end ()) {
-	return parseParticle (it, project, basedata);
+	return parseParticle (it, project, std::move (basedata));
     } else if (textIt != it.end ()) {
 	sLog.error ("Text objects are not supported yet");
     } else if (lightIt != it.end ()) {
 	sLog.error ("Light objects are not supported yet");
     } else {
 	// dump the object for now, might want to change later
+	// TODO: RE-EVALUATE IF THIS MAKES SENSE, THERE'S OBJECTS THAT CONTAIN OTHER OBJECTS AND THUS AREN'T REALLY
+	// ANYTHING SPECIAL
 	sLog.error ("Unknown object type found: ", it.dump ());
     }
 
-    return std::make_unique<Object> (basedata);
+    return std::make_unique<Object> (std::move (basedata));
 }
 
 std::vector<int> ObjectParser::parseDependencies (const JSON& it) {
@@ -106,7 +110,6 @@ ObjectParser::parseImage (const JSON& it, const Project& project, ObjectData bas
     auto result = std::make_unique<Image> (
 	std::move (base),
 	ImageData {
-	    .origin = it.user ("origin", properties, glm::vec3 (0.0f)),
 	    .scale = it.user ("scale", properties, glm::vec3 (1.0f)),
 	    .angles = it.user ("angles", properties, glm::vec3 (0.0)),
 	    .visible = it.user ("visible", properties, true),
@@ -258,7 +261,6 @@ ParticleUniquePtr ObjectParser::parseParticle (const JSON& it, const Project& pr
 	    return std::make_unique<Particle> (
 		std::move (base),
 		ParticleData {
-		    .origin = it.user ("origin", properties, glm::vec3 (0.0f)),
 		    .scale = it.user ("scale", properties, glm::vec3 (1.0f)),
 		    .angles = it.user ("angles", properties, glm::vec3 (0.0f)),
 		    .visible = it.user ("visible", properties, true),
@@ -508,7 +510,6 @@ ParticleUniquePtr ObjectParser::parseParticle (const JSON& it, const Project& pr
 	return std::make_unique<Particle> (
 	    std::move (base),
 	    ParticleData {
-		.origin = it.user ("origin", properties, glm::vec3 (0.0f)),
 		.scale = it.user ("scale", properties, glm::vec3 (1.0f)),
 		.angles = it.user ("angles", properties, glm::vec3 (0.0f)),
 		.visible = it.user ("visible", properties, true),

+ 2 - 0
src/WallpaperEngine/Data/Utils/MemoryStream.h

@@ -1,6 +1,7 @@
 #pragma once
 
 #include <iostream>
+#include <memory>
 
 namespace WallpaperEngine::Data::Utils {
 struct MemoryStream : std::istream, private std::streambuf {
@@ -25,4 +26,5 @@ struct MemoryStream : std::istream, private std::streambuf {
 };
 
 using MemoryStreamSharedPtr = std::shared_ptr<MemoryStream>;
+using MemoryStreamUniquePtr = std::unique_ptr<MemoryStream>;
 }

+ 5 - 1
src/WallpaperEngine/Render/CFBO.cpp

@@ -122,4 +122,8 @@ uint32_t CFBO::getSpritesheetFrames () const {
 
 float CFBO::getSpritesheetDuration () const {
     return 0.0f; // FBOs don't have spritesheets
-}
+}
+
+void CFBO::incrementUsageCount () const { }
+void CFBO::decrementUsageCount () const { }
+void CFBO::update () const { }

+ 4 - 0
src/WallpaperEngine/Render/CFBO.h

@@ -34,6 +34,10 @@ public:
     [[nodiscard]] uint32_t getSpritesheetFrames () const override;
     [[nodiscard]] float getSpritesheetDuration () const override;
 
+    void incrementUsageCount () const override;
+    void decrementUsageCount () const override;
+    void update () const override;
+
 private:
     GLuint m_framebuffer = GL_NONE;
     GLuint m_depthbuffer = GL_NONE;

+ 5 - 1
src/WallpaperEngine/Render/CObject.cpp

@@ -9,8 +9,12 @@ using namespace WallpaperEngine::Render::Wallpapers;
 CObject::CObject (Wallpapers::CScene& scene, const Object& object) :
     Helpers::ContextAware (scene), m_scene (scene), m_object (object) { }
 
+void CObject::render () { }
+
 Wallpapers::CScene& CObject::getScene () const { return this->m_scene; }
 
 const AssetLocator& CObject::getAssetLocator () const { return this->getScene ().getAssetLocator (); }
 
-int CObject::getId () const { return this->m_object.id; }
+int CObject::getId () const { return this->m_object.id; }
+
+const Object& CObject::getObject () const { return this->m_object; }

+ 5 - 6
src/WallpaperEngine/Render/CObject.h

@@ -31,16 +31,15 @@ public:
 
     template <class T> [[nodiscard]] bool is () const { return typeid (*this) == typeid (T); }
 
-    virtual void render () = 0;
+    CObject (Wallpapers::CScene& scene, const Object& object);
+    virtual ~CObject () override = default;
+
+    virtual void render ();
 
     [[nodiscard]] Wallpapers::CScene& getScene () const;
     [[nodiscard]] const AssetLocator& getAssetLocator () const;
     [[nodiscard]] int getId () const;
-
-    virtual ~CObject () override = default;
-
-protected:
-    CObject (Wallpapers::CScene& scene, const Object& object);
+    [[nodiscard]] const Object& getObject () const;
 
 private:
     Wallpapers::CScene& m_scene;

+ 57 - 10
src/WallpaperEngine/Render/CTexture.cpp

@@ -1,23 +1,50 @@
 #include "CTexture.h"
 #include "WallpaperEngine/Logging/Log.h"
 
-#include <cstring>
 #include <lz4.h>
-#include <string>
 
 #define STB_IMAGE_IMPLEMENTATION
+#include "RenderContext.h"
+
 #include <stb_image.h>
 
 using namespace WallpaperEngine::Render;
 
-CTexture::CTexture (TextureUniquePtr header) : m_header (std::move (header)) {
+CTexture::CTexture (RenderContext& context, TextureUniquePtr header) :
+    Helpers::ContextAware (context), m_header (std::move (header)) {
     // ensure the header is parsed
     this->setupResolution ();
     const GLint internalFormat = this->setupInternalFormat ();
 
+    // videos are a bit special, they only have one framebuffer, one mipmap
+    if (this->m_header->isVideoMp4 || this->m_header->flags & TextureFlags_Video) {
+	if (this->m_header->images.empty () || this->m_header->images.begin ()->second.empty ()) {
+	    sLog.exception ("Cannot load video texture, no mipmaps found");
+	}
+
+	// generate the texture and set it up to be used by the player
+	this->m_textureID = new GLuint[1];
+	glGenTextures (1, this->m_textureID);
+	this->setupOpenGLParameters (0);
+
+	const auto mipmap = *this->m_header->images.begin ()->second.begin ();
+
+	this->m_player = std::make_unique<GLPlayer> (
+	    this->getContext (), this->m_textureID[0],
+	    std::make_unique<MemoryStreamProtocol> (mipmap->uncompressedData.get (), mipmap->uncompressedSize),
+	    this->m_header->textureWidth, this->m_header->textureHeight
+	);
+	// setup texture video player
+	this->m_player->setMuted ();
+	this->m_player->setVolume (0.0f);
+	this->m_player->setUntimed ();
+	// texture is ready, nothing else to do
+	return;
+    }
+
     // allocate texture ids list
     this->m_textureID = new GLuint[this->m_header->imageCount];
-    // ask opengl for the correct amount of textures
+    // ask opengl for the correct amount of textures and framebuffers
     glGenTextures (this->m_header->imageCount, this->m_textureID);
 
     for (const auto& [index, mipmaps] : this->m_header->images) {
@@ -33,10 +60,6 @@ CTexture::CTexture (TextureUniquePtr header) : m_header (std::move (header)) {
 	    const uint32_t bufferSize = mipmap->uncompressedSize;
 	    GLenum textureFormat = GL_RGBA;
 
-	    if (this->m_header->isVideoMp4 || this->m_header->flags & TextureFlags_Video) {
-	        sLog.exception ("MP4 textures are not supported yet");
-	    }
-
 	    if (this->m_header->freeImageFormat != FIF_UNKNOWN) {
 		int fileChannels;
 
@@ -84,8 +107,13 @@ CTexture::CTexture (TextureUniquePtr header) : m_header (std::move (header)) {
 }
 
 CTexture::~CTexture () {
-    for (uint32_t i = 0; i < this->m_header->imageCount; i++) {
-        glDeleteTextures (1, &this->m_textureID[i]);
+    // first release the player to prevent using null references
+    this->m_player.reset ();
+
+    if (this->m_header->isVideoMp4 || this->m_header->flags & TextureFlags_Video) {
+	glDeleteTextures (1, this->m_textureID);
+    } else {
+	glDeleteTextures (this->m_header->imageCount, this->m_textureID);
     }
 
     delete[] this->m_textureID;
@@ -137,6 +165,7 @@ GLint CTexture::setupInternalFormat () const {
 }
 
 void CTexture::setupOpenGLParameters (const uint32_t textureID) const {
+    // TODO: LABEL ELEMENTS TOO
     // bind the texture to assign information to it
     glBindTexture (GL_TEXTURE_2D, this->m_textureID[textureID]);
 
@@ -216,3 +245,21 @@ uint32_t CTexture::getSpritesheetRows () const { return this->getHeader ().sprit
 uint32_t CTexture::getSpritesheetFrames () const { return this->getHeader ().spritesheetFrames; }
 
 float CTexture::getSpritesheetDuration () const { return this->getHeader ().spritesheetDuration; }
+
+void CTexture::incrementUsageCount () const {
+    if (this->m_player) {
+	this->m_player->incrementUsageCount ();
+    }
+}
+
+void CTexture::decrementUsageCount () const {
+    if (this->m_player) {
+	this->m_player->decrementUsageCount ();
+    }
+}
+
+void CTexture::update () const {
+    if (this->m_player) {
+	this->m_player->render ();
+    }
+}

+ 31 - 4
src/WallpaperEngine/Render/CTexture.h

@@ -1,22 +1,28 @@
 #pragma once
 
+#include "Helpers/ContextAware.h"
 #include "TextureProvider.h"
 #include "WallpaperEngine/Data/Assets/Texture.h"
+#include "WallpaperEngine/VideoPlayback/MPV/GLPlayer.h"
 
 #include <GL/glew.h>
 #include <glm/vec4.hpp>
 #include <memory>
+#include <mpv/client.h>
+#include <mpv/render.h>
+#include <mpv/render_gl.h>
 #include <vector>
 
 namespace WallpaperEngine::Render {
+class RenderContext;
 using namespace WallpaperEngine::Data::Assets;
-
+using namespace WallpaperEngine::VideoPlayback::MPV;
 /**
  * A normal texture file in WallpaperEngine's format
  */
-class CTexture final : public TextureProvider {
+class CTexture final : public TextureProvider, public Helpers::ContextAware {
 public:
-    explicit CTexture (TextureUniquePtr header);
+    explicit CTexture (RenderContext& context, TextureUniquePtr header);
     ~CTexture () override;
 
     [[nodiscard]] GLuint getTextureID (uint32_t imageIndex) const override;
@@ -34,6 +40,25 @@ public:
     [[nodiscard]] uint32_t getSpritesheetFrames () const override;
     [[nodiscard]] float getSpritesheetDuration () const override;
 
+    /**
+     * Increments the usage count of the texture
+     *
+     * Directly controls playback for video CTextures, only started when at least one thing is using it
+     * Initializes mpv if needed and starts playback
+     */
+    void incrementUsageCount () const override;
+    /**
+     * Decrements the usage count of the texture
+     *
+     * Directly controls playback for video CTextures, only stopped when nothing is using it
+     * De-initializes mpv if needed
+     */
+    void decrementUsageCount () const override;
+    /**
+     * Some textures need to be updated
+     */
+    void update () const override;
+
 private:
     /**
      * @return The texture header
@@ -51,7 +76,7 @@ private:
     /**
      * Prepares openGL parameters for loading texture data
      */
-    void setupOpenGLParameters (const uint32_t textureID) const;
+    void setupOpenGLParameters (uint32_t textureID) const;
 
     /** The texture header */
     TextureUniquePtr m_header;
@@ -59,5 +84,7 @@ private:
     GLuint* m_textureID = nullptr;
     /** Resolution vector of the texture */
     glm::vec4 m_resolution {};
+    /** The video player in use */
+    GLPlayerUniquePtr m_player;
 };
 } // namespace WallpaperEngine::Assets

+ 0 - 3
src/WallpaperEngine/Render/CWallpaper.cpp

@@ -7,9 +7,6 @@
 #include "WallpaperEngine/Data/Model/Project.h"
 #include "WallpaperEngine/Data/Model/Wallpaper.h"
 
-#include <glm/glm.hpp>
-#include <glm/gtc/type_ptr.hpp>
-
 using namespace WallpaperEngine::Render;
 
 CWallpaper::CWallpaper (

+ 2 - 1
src/WallpaperEngine/Render/CWallpaper.h

@@ -163,6 +163,8 @@ protected:
     /** The FBO used for scene output */
     std::shared_ptr<const CFBO> m_sceneFBO = nullptr;
 
+    GLuint m_vaoBuffer = GL_NONE;
+
 private:
     /** The texture used for the scene output */
     GLuint m_texCoordBuffer = GL_NONE;
@@ -172,7 +174,6 @@ private:
     GLint g_Texture0 = GL_NONE;
     GLint a_Position = GL_NONE;
     GLint a_TexCoord = GL_NONE;
-    GLuint m_vaoBuffer = GL_NONE;
     /** The framebuffer to draw the background to */
     GLuint m_destFramebuffer = GL_NONE;
     /** Setups OpenGL's shaders for this wallpaper backbuffer */

+ 1 - 1
src/WallpaperEngine/Render/Drivers/GLFWOpenGLDriver.cpp

@@ -185,4 +185,4 @@ __attribute__ ((constructor)) void registerGLFWOpenGLDriver () {
 	    return std::make_unique<GLFWOpenGLDriver> ("wallpaperengine", context, application);
 	}
     );
-}
+}

+ 1 - 0
src/WallpaperEngine/Render/Drivers/VideoFactories.h

@@ -16,6 +16,7 @@ public:
     using FullscreenDetectorConstructionFunc
 	= std::function<std::unique_ptr<Detectors::FullScreenDetector> (ApplicationContext&, VideoDriver&)>;
     VideoFactories ();
+    ~VideoFactories () = default;
 
     static VideoFactories& get ();
 

+ 13 - 0
src/WallpaperEngine/Render/Objects/CImage.cpp

@@ -30,6 +30,11 @@ CImage::CImage (Wallpapers::CScene& scene, const Image& image) :
     glm::vec2 size = this->getSize ();
     glm::vec3 scale = this->getImage ().scale->value->getVec3 ();
 
+    // TODO: PROPERLY SUPPORT PARENTS, FOR NOW THIS SHOULD BE ENOUGH
+    if (this->m_image.parent.has_value ()) {
+	origin += this->getScene ().getObject (this->m_image.parent.value ())->getObject ().origin->value->getVec3 ();
+    }
+
     this->detectTexture ();
 
     // detect texture (if any)
@@ -208,9 +213,15 @@ CImage::CImage (Wallpapers::CScene& scene, const Image& image) :
     this->m_modelViewProjectionCopyInverse = glm::inverse (this->m_modelViewProjectionCopy);
     this->m_modelMatrix = glm::ortho<float> (0.0, size.x, 0.0, size.y);
     this->m_viewProjectionMatrix = glm::mat4 (1.0);
+
+    // ensure the input texture is marked as used
+    // this makes video playback start if it's not already
+    this->m_texture->incrementUsageCount ();
 }
 
 CImage::~CImage () {
+    this->m_texture->decrementUsageCount ();
+
     // delete passes first as they depend on the image's data
     for (auto* pass : this->m_passes) {
 	delete pass;
@@ -453,6 +464,8 @@ void CImage::render () {
 	return;
     }
 
+    // TODO: DO NOT DRAW IMAGES THAT ARE NOT VISIBLE AND NOTHING DEPENDS ON THEM
+
     glColorMask (true, true, true, true);
 
     // update the position if required

+ 1 - 0
src/WallpaperEngine/Render/RenderContext.cpp

@@ -59,4 +59,5 @@ std::shared_ptr<const TextureProvider> RenderContext::resolveTexture (const std:
 const std::map<std::string, std::shared_ptr<CWallpaper>>& RenderContext::getWallpapers () const {
     return this->m_wallpapers;
 }
+
 } // namespace WallpaperEngine::Render

+ 2 - 3
src/WallpaperEngine/Render/TextureCache.cpp

@@ -27,7 +27,6 @@ std::shared_ptr<const TextureProvider> TextureCache::resolve (const std::string&
 	    auto stream = BinaryReader (contents);
 
 	    // Create metadata loader lambda that captures the assetLocator
-	    // Note: Unlike texture() which prepends "materials/", readString() doesn't,
 	    // so we need to construct the full path here
 	    auto metadataLoader = [&project] (const std::string& metaFilename) -> std::string {
 		std::filesystem::path fullPath = std::filesystem::path ("materials") / metaFilename;
@@ -35,7 +34,7 @@ std::shared_ptr<const TextureProvider> TextureCache::resolve (const std::string&
 	    };
 
 	    auto parsedTexture = TextureParser::parse (stream, filename, metadataLoader);
-	    auto texture = std::make_shared<CTexture> (std::move (parsedTexture));
+	    auto texture = std::make_shared<CTexture> (this->getContext (), std::move (parsedTexture));
 
 	    this->store (filename, texture);
 
@@ -50,4 +49,4 @@ std::shared_ptr<const TextureProvider> TextureCache::resolve (const std::string&
 
 void TextureCache::store (const std::string& name, std::shared_ptr<const TextureProvider> texture) {
     this->m_textureCache.insert_or_assign (name, texture);
-}
+}

+ 19 - 0
src/WallpaperEngine/Render/TextureProvider.h

@@ -78,5 +78,24 @@ public:
      * @return Duration of spritesheet animation in seconds
      */
     [[nodiscard]] virtual float getSpritesheetDuration () const = 0;
+
+    /**
+     * Increments the usage count of the texture
+     *
+     * Directly controls playback for video CTextures, only started when at least one thing is using it
+     * Initializes mpv if needed and starts playback
+     */
+    virtual void incrementUsageCount () const = 0;
+    /**
+     * Decrements the usage count of the texture
+     *
+     * Directly controls playback for video CTextures, only stopped when nothing is using it
+     * De-initializes mpv if needed
+     */
+    virtual void decrementUsageCount () const = 0;
+    /**
+     * Allows the texture contents to be updated (for example, for video textures)
+     */
+    virtual void update () const = 0;
 };
 } // namespace WallpaperEngine::Render

+ 43 - 0
src/WallpaperEngine/Render/Wallpapers/CScene.cpp

@@ -171,6 +171,21 @@ Render::CObject* CScene::createObject (const Object& object) {
 	}
     }
 
+    // check if the item has any parent and also create it first
+    if (object.parent.has_value ()) {
+	int parentId = object.parent.value ();
+
+	const auto dep = std::ranges::find_if (this->getScene ().objects, [&parentId] (const auto& o) {
+	    return o->id == parentId;
+	});
+
+	if (dep == this->getScene ().objects.end ()) {
+	    sLog.exception ("Cannot find parent ", parentId, " for object ", object.id);
+	}
+
+	this->createObject (**dep);
+    }
+
     if (object.is<Image> ()) {
 	auto* image = new Objects::CImage (*this, *object.as<Image> ());
 
@@ -199,6 +214,9 @@ Render::CObject* CScene::createObject (const Object& object) {
 	}
 
 	renderObject = particle;
+    } else {
+	sLog.debug ("Unknown object type, creating placeholder, empty object: ", object.id);
+	renderObject = new CObject (*this, object);
     }
 
     if (renderObject != nullptr) {
@@ -262,6 +280,29 @@ void CScene::renderFrame (const glm::ivec4& viewport) {
 	    = glm::mix (this->m_parallaxDisplacement, (this->m_mousePosition * amount) * influence, delay);
     }
 
+    // update main textures for images
+    for (const auto& cur : this->m_objectsByRenderOrder) {
+	if (!cur->is<Objects::CImage> ()) {
+	    continue;
+	}
+
+	const Objects::CImage* image = cur->as<Objects::CImage> ();
+
+#if !NDEBUG
+	const std::string message = "Updating texture " + image->getImage ().model->filename;
+
+	glPushDebugGroup (GL_DEBUG_SOURCE_APPLICATION, 0, -1, message.c_str ());
+#endif
+
+	image->getTexture ()->update ();
+
+#if !NDEBUG
+	glPopDebugGroup ();
+#endif
+    }
+
+    // bind the vertex array
+    glBindVertexArray (this->m_vaoBuffer);
     // use the scene's framebuffer by default
     glBindFramebuffer (GL_FRAMEBUFFER, this->getWallpaperFramebuffer ());
     // ensure we render over the whole framebuffer
@@ -310,3 +351,5 @@ const glm::vec2* CScene::getMousePositionLast () const { return &this->m_mousePo
 const glm::vec2* CScene::getParallaxDisplacement () const { return &this->m_parallaxDisplacement; }
 
 const std::vector<CObject*>& CScene::getObjectsByRenderOrder () const { return this->m_objectsByRenderOrder; }
+
+const CObject* CScene::getObject (int id) const { return this->m_objects.at (id); }

+ 1 - 0
src/WallpaperEngine/Render/Wallpapers/CScene.h

@@ -33,6 +33,7 @@ public:
     const glm::vec2* getParallaxDisplacement () const;
 
     [[nodiscard]] const std::vector<CObject*>& getObjectsByRenderOrder () const;
+    [[nodiscard]] const CObject* getObject (int id) const;
 
 protected:
     void renderFrame (const glm::ivec4& viewport) override;

+ 26 - 134
src/WallpaperEngine/Render/Wallpapers/CVideo.cpp

@@ -1,113 +1,36 @@
 #include "CVideo.h"
-#include "WallpaperEngine/Logging/Log.h"
-
-#include <GL/glew.h>
 
 #include "WallpaperEngine/Data/Model/Project.h"
 #include "WallpaperEngine/Data/Model/Wallpaper.h"
+#include "WallpaperEngine/VideoPlayback/MPV/GLPlayer.h"
 
 using namespace WallpaperEngine;
 using namespace WallpaperEngine::Render;
 using namespace WallpaperEngine::Render::Wallpapers;
-
-void* get_proc_address (void* ctx, const char* name) {
-    return static_cast<CVideo*> (ctx)->getContext ().getDriver ().getProcAddress (name);
-}
+using namespace WallpaperEngine::VideoPlayback::MPV;
 
 CVideo::CVideo (
     const Wallpaper& wallpaper, RenderContext& context, AudioContext& audioContext,
     const WallpaperState::TextureUVsScaling& scalingMode, const uint32_t& clampMode
 ) : CWallpaper (wallpaper, context, audioContext, scalingMode, clampMode) {
-    double volume = this->getContext ().getApp ().getContext ().settings.audio.volume * 100.0 / 128.0;
-
-    // create mpv contexts
-    this->m_mpv = mpv_create ();
-
-    if (this->m_mpv == nullptr) {
-	sLog.exception ("Could not create mpv context");
-    }
-
-    mpv_set_option_string (this->m_mpv, "terminal", "yes");
-#if NDEBUG
-    mpv_set_option_string (this->m_mpv, "msg-level", "all=status,statusline=no");
-#else
-    mpv_set_option_string (this->m_mpv, "msg-level", "all=v");
-#endif
-    mpv_set_option_string (this->m_mpv, "input-cursor", "no");
-    mpv_set_option_string (this->m_mpv, "cursor-autohide", "no");
-    mpv_set_option_string (this->m_mpv, "config", "no");
-    mpv_set_option_string (this->m_mpv, "fbo-format", "rgba8");
-    mpv_set_option_string (this->m_mpv, "vo", "libmpv");
-
-    if (mpv_initialize (this->m_mpv) < 0) {
-	sLog.exception ("Could not initialize mpv context");
-    }
-
-    mpv_set_option_string (this->m_mpv, "hwdec", "auto");
-    mpv_set_option_string (this->m_mpv, "loop", "inf");
-    mpv_set_option (this->m_mpv, "volume", MPV_FORMAT_DOUBLE, &volume);
-
-    if (!this->getContext ().getApp ().getContext ().settings.audio.enabled) {
-	this->m_muted = true;
-	mpv_set_option_string (this->m_mpv, "mute", "yes");
-    }
-
-    // initialize gl context for mpv
-    mpv_opengl_init_params gl_init_params { get_proc_address, this };
-    mpv_render_param params[] { { MPV_RENDER_PARAM_API_TYPE, const_cast<char*> (MPV_RENDER_API_TYPE_OPENGL) },
-				{ MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params },
-				{ MPV_RENDER_PARAM_INVALID, nullptr } };
-
-    if (mpv_render_context_create (&this->m_mpvGl, this->m_mpv, params) < 0) {
-	sLog.exception ("Failed to initialize MPV's GL context");
-    }
-
-    const std::filesystem::path videopath
-	= this->getVideo ().project.assetLocator->physicalPath (this->getVideo ().filename);
-
-    // build the path to the video file
-    const char* command[] = { "loadfile", videopath.c_str (), nullptr };
-
-    if (mpv_command (this->m_mpv, command) < 0) {
-	sLog.exception ("Cannot load video to play");
-    }
-
-    if (!this->getContext ().getApp ().getContext ().settings.audio.enabled) {
-	const char* mutecommand[] = { "set", "mute", "yes", nullptr };
-
-	mpv_command (this->m_mpv, mutecommand);
-    }
-
     // setup framebuffers
     this->setupFramebuffers ();
-}
 
-CVideo::~CVideo () {
-    // stop mpv and cleanup resources
-    if (this->m_mpvGl) {
-	mpv_render_context_free (this->m_mpvGl);
-	this->m_mpvGl = nullptr;
-    }
+    const std::filesystem::path videopath
+	= this->getVideo ().project.assetLocator->physicalPath (this->getVideo ().filename);
 
-    if (this->m_mpv) {
-	mpv_terminate_destroy (this->m_mpv);
-	this->m_mpv = nullptr;
-    }
+    // create a player with a small framebuffer
+    // this will be changed after mpv starts playback and sees the video resolution
+    this->m_player = std::make_unique<GLPlayer> (
+	this->getContext (), this->CWallpaper::getWallpaperTexture (), videopath, 64, 64,
+	this->CWallpaper::getWallpaperFramebuffer ()
+    );
+    this->m_player->setVolume (this->getContext ().getApp ().getContext ().settings.audio.volume * 100.0 / 128.0);
+    // make sure the video has at least one usage marked, this ensures the video plays
+    this->m_player->incrementUsageCount ();
 }
 
-void CVideo::setSize (const int width, const int height) {
-    this->m_width = width > 0 ? width : this->m_width;
-    this->m_height = height > 0 ? height : this->m_height;
-
-    // do not refresh the texture if any of the sizes are invalid
-    if (this->m_width <= 0 || this->m_height <= 0) {
-	return;
-    }
-
-    // reconfigure the texture
-    glBindTexture (GL_TEXTURE_2D, this->getWallpaperTexture ());
-    glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, this->m_width, this->m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
-}
+CVideo::~CVideo () { this->m_player->decrementUsageCount (); }
 
 void CVideo::renderFrame (const glm::ivec4& viewport) {
     // ensure the video's audio follows audio detection rules
@@ -115,57 +38,26 @@ void CVideo::renderFrame (const glm::ivec4& viewport) {
 	&& this->m_muted != this->getAudioContext ().getDriver ().getAudioDetector ().anythingPlaying ()) {
 	this->m_muted = !this->m_muted;
 
-	const char* mutecommand[] = { "set", "mute", this->m_muted ? "yes" : "no", nullptr };
-
-	mpv_command (this->m_mpv, mutecommand);
-    }
-
-    // read any and all the events available
-    while (this->m_mpv) {
-	const mpv_event* event = mpv_wait_event (this->m_mpv, 0);
-
-	if (event == nullptr || event->event_id == MPV_EVENT_NONE) {
-	    break;
-	}
-
-	// we do not care about any of the events
-	if (event->event_id == MPV_EVENT_VIDEO_RECONFIG) {
-	    int64_t width, height;
-
-	    if (mpv_get_property (this->m_mpv, "dwidth", MPV_FORMAT_INT64, &width) >= 0
-		&& mpv_get_property (this->m_mpv, "dheight", MPV_FORMAT_INT64, &height) >= 0) {
-		this->setSize (width, height);
-	    }
+	if (this->m_muted) {
+	    this->m_player->setMuted ();
+	} else {
+	    this->m_player->clearMuted ();
 	}
     }
 
-    // render the next
-    glViewport (0, 0, this->getWidth (), this->getHeight ());
-
-    mpv_opengl_fbo fbo { static_cast<int> (this->getWallpaperFramebuffer ()), static_cast<int> (this->m_width),
-			 static_cast<int> (this->m_height), GL_RGBA8 };
-
-    // no need to flip as it'll be handled by the wallpaper rendering code
-    int flip_y = 0;
-
-    mpv_render_param params[] = { { MPV_RENDER_PARAM_OPENGL_FBO, &fbo },
-				  { MPV_RENDER_PARAM_FLIP_Y, &flip_y },
-				  { MPV_RENDER_PARAM_INVALID, nullptr } };
-
-    mpv_render_context_render (this->m_mpvGl, params);
+    this->m_player->render ();
 }
 
-const Video& CVideo::getVideo () const { return *this->getWallpaperData ().as<Video> (); }
+const Data::Model::Video& CVideo::getVideo () const { return *this->getWallpaperData ().as<Data::Model::Video> (); }
 
 void CVideo::setPause (bool newState) {
-    if (m_paused == newState) {
-	return;
+    if (newState) {
+	this->m_player->setPaused ();
+    } else {
+	this->m_player->clearPaused ();
     }
-    m_paused = newState;
-    int pause = newState;
-    mpv_set_property (m_mpv, "pause", MPV_FORMAT_FLAG, &pause);
 }
 
-int CVideo::getWidth () const { return this->m_width; }
+int CVideo::getWidth () const { return this->m_player ? this->m_player->getWidth () : 16; }
 
-int CVideo::getHeight () const { return this->m_height; }
+int CVideo::getHeight () const { return this->m_player ? this->m_player->getHeight () : 16; }

+ 5 - 9
src/WallpaperEngine/Render/Wallpapers/CVideo.h

@@ -2,10 +2,11 @@
 
 #include "WallpaperEngine/Audio/AudioStream.h"
 #include "WallpaperEngine/Render/CWallpaper.h"
-#include <mpv/client.h>
-#include <mpv/render_gl.h>
+#include "WallpaperEngine/VideoPlayback/MPV/GLPlayer.h"
 
 namespace WallpaperEngine::Render::Wallpapers {
+using namespace WallpaperEngine::VideoPlayback::MPV;
+
 class CVideo final : public CWallpaper {
 public:
     CVideo (
@@ -15,13 +16,12 @@ public:
 
     ~CVideo () override;
 
-    const Video& getVideo () const;
+    const Data::Model::Video& getVideo () const;
 
     [[nodiscard]] int getWidth () const override;
     [[nodiscard]] int getHeight () const override;
 
     void setPause (bool newState) override;
-    void setSize (int width, int height);
 
 protected:
     void renderFrame (const glm::ivec4& viewport) override;
@@ -29,12 +29,8 @@ protected:
     friend class CWallpaper;
 
 private:
-    mpv_handle* m_mpv = nullptr;
-    mpv_render_context* m_mpvGl = nullptr;
+    GLPlayerUniquePtr m_player;
 
-    bool m_paused = false;
-    int64_t m_width = 16;
-    int64_t m_height = 16;
     bool m_muted = false;
 };
 } // namespace WallpaperEngine::Render::Wallpapers

+ 296 - 0
src/WallpaperEngine/VideoPlayback/MPV/GLPlayer.cpp

@@ -0,0 +1,296 @@
+#include "GLPlayer.h"
+
+#include "WallpaperEngine/Logging/Log.h"
+
+#include <mpv/render_gl.h>
+#include <mpv/stream_cb.h>
+
+using namespace WallpaperEngine::VideoPlayback::MPV;
+
+void* get_proc_address (void* ctx, const char* name) {
+    return static_cast<GLPlayer*> (ctx)->getContext ().getDriver ().getProcAddress (name);
+}
+
+GLPlayer::GLPlayer (
+    RenderContext& context, GLuint outputTexture, const std::filesystem::path& file, const int64_t baseWidth,
+    const int64_t baseHeight, const GLuint fbo
+) : ContextAware (context), m_outputTexture (outputTexture), m_width (baseWidth), m_height (baseHeight) {
+    this->m_fbo = fbo;
+    this->m_doWeOwnFramebuffer = this->m_fbo == GL_NONE;
+
+    this->prepareGL ();
+    this->setSource (file);
+}
+
+GLPlayer::GLPlayer (
+    RenderContext& context, const GLuint outputTexture, MemoryStreamProtocolUniquePtr stream, const int64_t baseWidth,
+    const int64_t baseHeight, const GLuint fbo
+) : ContextAware (context), m_outputTexture (outputTexture), m_width (baseWidth), m_height (baseHeight) {
+    this->m_fbo = fbo;
+    this->m_doWeOwnFramebuffer = this->m_fbo == GL_NONE;
+
+    this->prepareGL ();
+    this->setSource (std::move (stream));
+}
+
+GLPlayer::~GLPlayer () {
+    // clean up any kept resources
+    this->stop ();
+
+    // only clean up framebuffer if we own it
+    if (this->m_doWeOwnFramebuffer) {
+	// free gl resources too
+	glDeleteFramebuffers (1, &this->m_fbo);
+    }
+}
+
+void GLPlayer::incrementUsageCount () {
+    this->m_usageCount++;
+
+    if (this->m_usageCount == 1) {
+	this->play ();
+    }
+}
+
+void GLPlayer::decrementUsageCount () {
+    if (this->m_usageCount == 0) {
+	sLog.exception ("GLPlayer usage count would underflow");
+    }
+
+    this->m_usageCount--;
+
+    if (this->m_usageCount == 0) {
+	this->stop ();
+    }
+}
+
+void GLPlayer::setUntimed () {
+    if (this->m_handle) {
+	sLog.exception ("Cannot set untimed mode after playback has started");
+    }
+
+    this->m_untimed = true;
+}
+
+void GLPlayer::clearUntimed () {
+    if (this->m_handle) {
+	sLog.exception ("Cannot set untimed mode after playback has started");
+    }
+
+    this->m_untimed = false;
+}
+
+void GLPlayer::setMuted () {
+    this->m_muted = true;
+
+    if (this->m_handle) {
+	mpv_set_property_string (this->m_handle, "mute", "yes");
+    }
+}
+
+void GLPlayer::clearMuted () {
+    this->m_muted = false;
+
+    if (this->m_handle) {
+	mpv_set_property_string (this->m_handle, "mute", "no");
+    }
+}
+
+void GLPlayer::setVolume (double volume) {
+    this->m_volume = volume;
+
+    if (this->m_handle) {
+	mpv_set_property (this->m_handle, "volume", MPV_FORMAT_DOUBLE, &this->m_volume);
+    }
+}
+
+void GLPlayer::setPaused () {
+    this->m_paused = true;
+
+    if (this->m_handle) {
+	mpv_set_property_string (this->m_handle, "pause", "yes");
+    }
+}
+
+void GLPlayer::clearPaused () {
+    this->m_paused = false;
+
+    if (this->m_handle) {
+	mpv_set_property_string (this->m_handle, "pause", "no");
+    }
+}
+
+void GLPlayer::render () const {
+    // rendering should only happen if the texture is in use
+    if (this->m_handle == nullptr) {
+	return;
+    }
+
+    // read all the events available
+    while (true) {
+	const mpv_event* event = mpv_wait_event (this->m_handle, 0);
+
+	if (event == nullptr || event->event_id == MPV_EVENT_NONE) {
+	    break;
+	}
+
+	if (event->event_id != MPV_EVENT_VIDEO_RECONFIG) {
+	    continue;
+	}
+
+	int64_t width, height;
+
+	if (mpv_get_property (this->m_handle, "dwidth", MPV_FORMAT_INT64, &width) < 0) {
+	    continue;
+	}
+
+	if (mpv_get_property (this->m_handle, "dheight", MPV_FORMAT_INT64, &height) < 0) {
+	    continue;
+	}
+
+	if (width < 0 || height < 0) {
+	    continue;
+	}
+
+	this->m_width = width;
+	this->m_height = height;
+	// reconfigure the texture
+	glBindTexture (GL_TEXTURE_2D, this->m_outputTexture);
+	glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, this->m_width, this->m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    }
+
+    // render the next
+    glViewport (0, 0, this->m_width, this->m_height);
+
+    mpv_opengl_fbo fbo { static_cast<int> (this->m_fbo), static_cast<int> (this->m_width),
+			 static_cast<int> (this->m_height), GL_RGBA8 };
+
+    // no need to flip as it'll be handled by the wallpaper rendering code
+    int flip_y = 0;
+
+    mpv_render_param params[] = { { MPV_RENDER_PARAM_OPENGL_FBO, &fbo },
+				  { MPV_RENDER_PARAM_FLIP_Y, &flip_y },
+				  { MPV_RENDER_PARAM_INVALID, nullptr } };
+
+    mpv_render_context_render (this->m_renderContext, params);
+}
+
+int GLPlayer::getWidth () const { return this->m_width; }
+int GLPlayer::getHeight () const { return this->m_height; }
+
+void GLPlayer::prepareGL () {
+    if (!this->m_doWeOwnFramebuffer || this->m_fbo != GL_NONE) {
+	return;
+    }
+
+    glGenFramebuffers (1, &this->m_fbo);
+    glBindFramebuffer (GL_FRAMEBUFFER, this->m_fbo);
+    glBindTexture (GL_TEXTURE_2D, this->m_outputTexture);
+    // reset texture's contents
+    glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, this->m_width, this->m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    constexpr GLenum drawBuffers[1] = { GL_COLOR_ATTACHMENT0 };
+    glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, this->m_outputTexture, 0);
+    glDrawBuffers (1, drawBuffers);
+
+    // ensure first framebuffer is okay
+    if (glCheckFramebufferStatus (GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+	sLog.exception ("Framebuffers are not properly set");
+    }
+
+    // clear the framebuffer
+    glClear (GL_COLOR_BUFFER_BIT);
+}
+
+void GLPlayer::init () {
+    this->m_handle = mpv_create ();
+
+    if (this->m_handle == nullptr) {
+	sLog.exception ("Cannot create mpv context for video texture");
+    }
+
+    // setup mpv options for playback
+    mpv_set_option_string (this->m_handle, "terminal", "yes");
+#if NDEBUG
+    mpv_set_option_string (this->m_handle, "msg-level", "all=status,statusline=no");
+#else
+    mpv_set_option_string (this->m_handle, "msg-level", "all=v");
+#endif
+    mpv_set_option_string (this->m_handle, "input-cursor", "no");
+    mpv_set_option_string (this->m_handle, "cursor-autohide", "no");
+    mpv_set_option_string (this->m_handle, "config", "no");
+    mpv_set_option_string (this->m_handle, "fbo-format", "rgba8");
+    mpv_set_option_string (this->m_handle, "vo", "libmpv");
+    mpv_set_option_string (this->m_handle, "profile", "fast");
+    mpv_set_option_string (this->m_handle, "untimed", this->m_untimed ? "yes" : "no");
+
+    if (mpv_initialize (this->m_handle) < 0) {
+	sLog.exception ("Could not initialize mpv context");
+    }
+
+    // ensure video is muted and plays in a loop
+    mpv_set_property_string (this->m_handle, "hwdec", "auto");
+    mpv_set_property_string (this->m_handle, "loop", "inf");
+    mpv_set_property (this->m_handle, "volume", MPV_FORMAT_DOUBLE, &this->m_volume);
+
+    // initialize gl context for mpv
+    mpv_opengl_init_params gl_init_params { get_proc_address, this };
+    mpv_render_param params[] { { MPV_RENDER_PARAM_API_TYPE, const_cast<char*> (MPV_RENDER_API_TYPE_OPENGL) },
+				{ MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params },
+				{ MPV_RENDER_PARAM_INVALID, nullptr } };
+
+    if (mpv_render_context_create (&this->m_renderContext, this->m_handle, params) < 0) {
+	sLog.exception ("Failed to initialize MPV's GL context");
+    }
+
+    // mute the video if required
+    mpv_set_property_string (this->m_handle, "mute", this->m_muted ? "yes" : "no");
+    // ensure play/pause status is respected too
+    mpv_set_property_string (this->m_handle, "pause", this->m_paused ? "yes" : "no");
+}
+
+void GLPlayer::setSource (const std::filesystem::path& file) { this->m_file = file; }
+
+void GLPlayer::setSource (MemoryStreamProtocolUniquePtr source) { this->m_stream = std::move (source); }
+
+void GLPlayer::play () {
+    if (this->m_handle != nullptr) {
+	sLog.exception ("Cannot play the same GLPlayer twice");
+    }
+
+    if (!this->m_file.has_value () && !this->m_stream.has_value ()) {
+	sLog.exception ("Cannot play a GLPlayer without a source");
+    }
+
+    this->init ();
+
+    if (this->m_file.has_value ()) {
+	// build the path to the video file
+	const char* command[] = { "loadfile", this->m_file.value ().c_str (), nullptr };
+
+	if (mpv_command (this->m_handle, command) < 0) {
+	    sLog.exception ("Cannot load video to play");
+	}
+    } else if (this->m_stream) {
+	this->m_stream.value ()->registerReadCallback (this->m_handle);
+
+	// start playing the video
+	const char* command[] = { "loadfile", "buffer://", nullptr };
+
+	if (mpv_command (this->m_handle, command) < 0) {
+	    sLog.exception ("Cannot load video texture to play");
+	}
+    }
+}
+
+void GLPlayer::stop () {
+    // clean up mpv and get it ready to start again at some point
+    if (this->m_renderContext) {
+	mpv_render_context_free (this->m_renderContext);
+	this->m_renderContext = nullptr;
+    }
+
+    if (this->m_handle) {
+	mpv_terminate_destroy (this->m_handle);
+	this->m_handle = nullptr;
+    }
+}

+ 81 - 0
src/WallpaperEngine/VideoPlayback/MPV/GLPlayer.h

@@ -0,0 +1,81 @@
+#pragma once
+
+#include "MemoryStreamProtocol.h"
+#include "WallpaperEngine/Render/RenderContext.h"
+
+#include <GL/glew.h>
+#include <mpv/client.h>
+#include <mpv/render.h>
+#include <string>
+
+namespace WallpaperEngine::VideoPlayback::MPV {
+class GLPlayer : public Helpers::ContextAware {
+    friend struct MemoryStreamProtocol;
+
+public:
+    GLPlayer (
+	RenderContext& context, GLuint outputTexture, const std::filesystem::path& file, int64_t baseWidth,
+	int64_t baseHeight, GLuint fbo = GL_NONE
+    );
+    GLPlayer (
+	RenderContext& context, GLuint outputTexture, MemoryStreamProtocolUniquePtr stream, int64_t baseWidth,
+	int64_t baseHeight, GLuint fbo = GL_NONE
+    );
+    ~GLPlayer () override;
+
+    /**
+     * Increments the usage count of the player
+     *
+     * Directly controls playback, only started when at least one thing is using it
+     * Initializes mpv if needed and starts playback
+     */
+    void incrementUsageCount ();
+    /**
+     * Decrements the usage count of the player
+     *
+     * Directly controls playback, only stopped when nothing is using it
+     * De-initializes mpv if needed
+     */
+    void decrementUsageCount ();
+
+    void setUntimed ();
+    void clearUntimed ();
+    void setMuted ();
+    void clearMuted ();
+    void setVolume (double volume);
+    void setPaused ();
+    void clearPaused ();
+
+    void render () const;
+
+    int getWidth () const;
+    int getHeight () const;
+
+private:
+    void prepareGL ();
+    void init ();
+    void play ();
+    void setSource (MemoryStreamProtocolUniquePtr source);
+    void setSource (const std::filesystem::path& file);
+    void stop ();
+
+protected:
+    bool m_doWeOwnFramebuffer;
+    GLuint m_outputTexture;
+    GLuint m_fbo = GL_NONE;
+    mutable int64_t m_width;
+    mutable int64_t m_height;
+    mpv_handle* m_handle = nullptr;
+    mpv_render_context* m_renderContext = nullptr;
+    double m_volume = 0.0f;
+    bool m_muted = false;
+    bool m_untimed = false;
+    bool m_paused = false;
+    std::optional<std::filesystem::path> m_file;
+    std::optional<MemoryStreamProtocolUniquePtr> m_stream;
+    uint32_t m_usageCount = 0;
+};
+
+using GLPlayerUniquePtr = std::unique_ptr<GLPlayer>;
+using GLPlayerSharedPtr = std::shared_ptr<GLPlayer>;
+};

+ 54 - 0
src/WallpaperEngine/VideoPlayback/MPV/MemoryStreamProtocol.cpp

@@ -0,0 +1,54 @@
+#include "MemoryStreamProtocol.h"
+
+#include "GLPlayer.h"
+
+#include <mpv/stream_cb.h>
+
+using namespace WallpaperEngine::VideoPlayback::MPV;
+
+int64_t mem_seek (void* cookie, const int64_t offset) {
+    const auto stream = static_cast<MemoryStreamProtocol*> (cookie);
+
+    // sometimes the stream can get to fail state depending on what mpv did
+    // seeking usually means we can ignore that and try seek
+    // if the seek fails the failbit will be set again
+    if (stream->fail ()) {
+	stream->clear ();
+    }
+
+    stream->seekg (offset, std::ios_base::beg);
+
+    return stream->tellg ();
+}
+
+int64_t mem_read (void* cookie, char* buf, uint64_t bytes) {
+    return static_cast<MemoryStreamProtocol*> (cookie)->read (buf, bytes).gcount ();
+}
+
+int64_t mem_size (void* cookie) { return static_cast<MemoryStreamProtocol*> (cookie)->m_size; }
+
+void mem_close (void* cookie) {
+    // seek to the beginning again for the next play
+    mem_seek (cookie, 0);
+    // closing does nothing else because the life is managed
+    // by whatever owns it
+
+    // this also allows starting playback in another instance no problem,
+    // but two instances won't be able to use the same stream
+}
+
+int mem_open (void* userdata, char* uri, struct mpv_stream_cb_info* info) {
+    info->cookie = userdata;
+    info->read_fn = mem_read;
+    info->seek_fn = mem_seek;
+    info->close_fn = mem_close;
+    info->size_fn = mem_size;
+
+    return 0;
+}
+
+void MemoryStreamProtocol::registerReadCallback (mpv_handle* handle) {
+    if (mpv_stream_cb_add_ro (handle, "buffer", this, mem_open) < 0) {
+	sLog.exception ("Cannot register memory stream protocol for mpv");
+    }
+}

+ 24 - 0
src/WallpaperEngine/VideoPlayback/MPV/MemoryStreamProtocol.h

@@ -0,0 +1,24 @@
+#pragma once
+#include "WallpaperEngine/Data/Utils/MemoryStream.h"
+
+#include <mpv/client.h>
+
+#include <cstring>
+#include <memory>
+
+namespace WallpaperEngine::VideoPlayback::MPV {
+class GLPlayer;
+struct MemoryStreamProtocol : Data::Utils::MemoryStream {
+    explicit MemoryStreamProtocol (const char* pointer, const size_t size) :
+	MemoryStream (std::unique_ptr<char[]> (new char[size]), size), m_size (size) {
+	memcpy (this->m_buffer.get (), pointer, size);
+    }
+
+    void registerReadCallback (mpv_handle* handle);
+
+    size_t m_size;
+};
+
+using MemoryStreamProtocolUniquePtr = std::unique_ptr<MemoryStreamProtocol>;
+using MemoryStreamProtocolSharedPtr = std::shared_ptr<MemoryStreamProtocol>;
+};