Why This Project Exists
I had a folder of downloaded video courses and no good way to actually work through them. A generic media player forgets where you were, knows nothing about sections or lectures, and cannot tell you whether you have finished a course. The web players these courses come from need an account and a connection. What I wanted was something in between: a player that understands course structure, remembers everything, and works on a plane.
So Deskemy is a desktop app that treats a folder of videos as a first-class course. It reads the folder structure into sections and ordered lectures, cleans the numeric prefixes out of the titles, attaches subtitles and resource files to the lecture they belong to, and then gets out of the way. Your files are referenced where they sit — never copied, moved, or uploaded — and everything the app records lives in one local SQLite database.
My Role
Solo project, end to end: the Rust core, both front ends, the playback integration, the SQLite schema and search, the installers, the signed update feed, and the release process. The interesting engineering was almost all at the boundaries — between a user interface and a native video surface, and between a media library and the filesystem underneath it.
Architecture
Since 2.0, Deskemy is a single native Rust application. The interface is built in Slint, rendered by Skia over OpenGL. Underneath it sits a platform- and UI-agnostic core crate that owns the database, the importer, configuration, backups, and the libmpv bindings. Video is decoded by libmpv and drawn straight into the interface's own OpenGL context, which is the decision the rest of this page is about.
flowchart TB
subgraph win[Deskemy window - Slint]
ui[Slint UI - Skia over OpenGL]
subgraph core[Rust core - deskemy-core]
pages[Pages and session]
imp[Two-phase importer: scan, probe, persist]
ply[Player control via FFI]
end
end
mpv[libmpv-2.dll bundled]
db[(SQLite + FTS5 via rusqlite)]
thumb[(Content-addressed thumbnail cache)]
files[/Your course folders, referenced in place/]
ui -->|callbacks| pages
pages --> imp
pages --> ply
pages --> db
pages --> thumb
imp -->|probe| mpv
imp --> files
imp --> db
ply --> mpv
ply --> db
mpv -->|renders frames into the UI GL context| ui
Three Attempts at One Problem
A course player needs controls, a seek bar, subtitles and a contents panel drawn over the video. That sounds trivial until the video is not rendered by the same thing as the interface. Every version of Deskemy has been an answer to that one problem, and each answer was forced by a real constraint in the one before it.
flowchart LR
a[First version: mpv in a child window raised above WebView2]
b[1.0.x: mpv texture in a DirectComposition layer beneath a transparent WebView2]
c[2.0: mpv draws into the Slint UI OpenGL context]
a -->|video lags the UI on every resize| b
b -->|Windows only, JS reports the video rect| c
Approach: the first version was a Tauri app with a SvelteKit interface in a WebView2 window. mpv rendered into its own child window, raised above the WebView.
Problem: those are two separate surfaces, and their resizes cannot be atomic. The WebView resizes instantly on the GPU compositor; the video window is repositioned through JavaScript, IPC, and a Win32 call, landing one or two frames behind. On every fullscreen toggle and panel open, the picture visibly chased the layout. Two mitigations were tried and rejected: a reflow that rubber-banded, and a mask that blacked the video out during resizes, which was worse than the bug.
Approach: stop having two surfaces. mpv's render API draws into a Direct3D texture owned by the app, set as the content of a DirectComposition layer beneath a transparent WebView2 on the same window. The OS composites both, so resizing becomes a single atomic operation.
The gate: the whole plan hinged on one unknown — whether a composition layer would show through a WebView that Tauri hosts as a window rather than as a composition visual. If not, the fix meant forking Tauri's web view layer. So the first step was a solid magenta layer, with no mpv involved at all. It showed through, and the approach went ahead without a fork.
Shipping it safely: a side-effect-free GPU probe runs once at startup. If it passes, the player composites; if it fails — old GPUs, some virtual machines, Remote Desktop — it falls back automatically to the child-window player. The rule was never to ship a broken player, and the fallback is what kept it.
Problem: the compositor worked, but it was Windows only, and it still relied on JavaScript reporting the video's rectangle on every layout change. That combination was the blocker for Linux and macOS.
Approach: in Slint, mpv's OpenGL render API draws into a texture inside the interface's own GL context, displayed by an ordinary image element. Overlays are simply elements declared after it — the same code on every platform, with no rectangle plumbing at all. The layering problem stops existing instead of being solved again.
Alternatives: Iced was ruled out for having no
accessibility support, no API for foreign textures, and yearly breaking changes. Flutter
meant a Dart rewrite plus an FFI bridge. A plain HTML <video> element only
plays browser-friendly codecs, and people's course libraries are not all H.264.
Engineering Highlights
Problem: downloaded course videos are whatever the uploader exported — arbitrary containers, codecs, and subtitle formats. A browser-grade video element supports a narrow slice of that, so a web-based player would either refuse to play files or force people to transcode their library.
Implementation: playback goes through
libmpv, loaded at runtime through FFI with libloading rather
than linked at build time, so codec coverage is mpv's.
libmpv-2.dll ships inside every installer and the portable zip, and the loader
falls back to a copy on PATH or at DESKEMY_LIBMPV. The playback
tests run against a headless mpv.
Problem: replacing the entire front end of an app people use every day is the classic way to spend months with nothing shippable.
Implementation: the first step was extracting a core crate with no knowledge of any interface — database, import, configuration, backup, and the libmpv bindings — and making the shipping Tauri app depend on it. Both apps then shared one core while the new one caught up, and the old one kept shipping. The two were deliberately not joined into a single Cargo workspace, because that would have moved the Tauri app's build directory and broken its build script and release flow. The Slint app can also render any screen offscreen to a PNG with no window, which made layout checks scriptable.
Problem: every 1.x install checks for updates. A rewrite that loses someone's library, or quietly installs alongside the old version, breaks trust with the exact people who made the project worth continuing.
Implementation: 1.x installs are offered 2.0 as an ordinary update, and the MSI upgrades an existing 1.x MSI install in place. The first launch migrates the library database forward in a way 1.x can still open. Every installer is signed, the app only accepts updates that verify against its embedded public key, and a verification step checks both installers against that key before a release is published. The whole check, download, verify, restart and install sequence can be rehearsed locally by pointing the app at a manifest served from disk.
Problem: importing a course means probing every video for duration and track metadata. Probing is slow and I/O-bound. Doing it while holding the database connection would serialize the whole import behind media inspection and leave the interface unresponsive on a large folder.
Implementation: the importer runs in two phases — probe, then persist. Probing runs on a worker thread off the database connection, with live progress, and only the resulting metadata is written in the persist phase. The user sees a preview of exactly what will be imported first, and the probed plan is kept, so confirming does not probe everything a second time.
Problem: if a library row is keyed by file path, reorganizing your folders throws away your history. Rename a directory and every lecture underneath it looks brand new — progress gone, bookmarks orphaned.
Implementation: lectures are matched by content rather than path, so a moved or renamed file keeps its watch progress and bookmarks, and adding a course folder again updates it rather than duplicating it. The thumbnail cache is content-addressed on the same principle, and an optional folder watcher picks up changes on its own.
Problem: title search only finds a lecture if you remember roughly what it was called. Often you remember a phrase the instructor said and have no idea which of two hundred lectures it was in.
Implementation: SQLite FTS5,
bundled through rusqlite so there is no external database, indexes course,
section and lecture titles. Optional subtitle search indexes the spoken
text from your subtitle files and jumps straight to the matching timestamp, with the matched
words highlighted.
Problem: "portable" is a promise about more than the
executable. An app that claims to run from a USB stick but quietly writes to
%APPDATA% and the registry has left traces where it promised not to.
Implementation: the installed build keeps its
library under %APPDATA% and deliberately leaves it behind on uninstall, so a
reinstall resumes where you left off. The portable build detects a .portable
marker beside the executable and redirects all state into a data/
folder next to it. Since a portable copy cannot safely replace itself, it points you to
the release page instead of self-updating.
Study Tools
Beyond playback, the app tracks the things that make a course feel finishable. Continue Watching resumes the exact lecture. Resources such as exercise PDFs and project folders sit under the lecture they belong to and can be ticked off, and autoplay can optionally stop at a lecture with exercises to offer them. There are timestamped bookmarks, tags, favorites, a watch history, Career Tracks for ordering courses into a path, a sleep timer, and a full set of YouTube-style keyboard shortcuts.
2.0 adds a mini player: press T and the player shrinks to a small
always-on-top window beside your editor or terminal. It keeps the video's shape, remembers where
you left it, and has its own play, skip and seek controls on hover — which matters for courses
where you type along with the instructor.
? away at any time.Privacy By Construction
There are no accounts, no telemetry, and no network requests for your content. The library, progress, bookmarks and stats exist only in a local database, and the app works entirely offline. The single request it makes is the update check: it reads the latest release's manifest from GitHub, downloads nothing unless you click Update, and installs nothing that fails signature verification.
Tradeoffs And Honest Limits
2.0 is released for Windows only. The Linux build compiles and passes its tests in CI on Ubuntu against the system libmpv, including an offscreen render, but it will not ship until it has been run on real hardware under X11 and Wayland with hardware decoding. macOS is a genuine risk rather than a formality: Apple has deprecated OpenGL, and I do not know of anyone who has shipped mpv and Slint there yet.
Moving off the WebView traded one requirement for another: Deskemy no longer needs Microsoft's WebView2 runtime, but it now needs a GPU with OpenGL 3.3, which any integrated GPU from the last decade has. Text was a real cost too. Slint's stock renderers draw grayscale text, which looks rough next to ClearType at 100% scaling, so Deskemy carries a patch to Slint's Skia renderer for subpixel text on Windows — and that patch has to be reapplied on every Slint upgrade.
The database migrates forward in a way 1.x can still open, but a backup exported from 2.0 will not import into 1.x. Bundling libmpv makes the download larger than a web-stack app would be. Subtitle search only knows what is in your subtitle files, so a course shipped without them is not searchable by speech. And community translations are maintained as separate projects, so they can trail a release.
What This Demonstrates
This is the project where I had to be most careful at the seams between an application and the operating system: loading a C library at runtime, getting native video and an interface to share a screen without fighting, and deciding what identity means for a file that can move. It is also the one with real users, which changed how I worked. The 1.0.x compositor was gated on a magenta rectangle before any real code was written, it shipped behind an automatic fallback, and 2.0 replaced the whole interface without making anyone reinstall or lose a single bookmark.