About Projects Experience Contact
← Back to Projects

Desktop systems case study / shipped product / rebuilt for 2.0

Deskemy

An offline player for the video courses you already own. Point it at a folder and it becomes a browsable library with native playback, resume, subtitle search, and study tools. 2.0 is a complete rebuild of the interface, and the reason for it is the most interesting engineering in the project.

52 GitHub stars from real users
343 installer and portable downloads
3 video pipelines built for one problem
0 accounts or telemetry
Deskemy library home screen showing Continue Watching and the course grid
The library: Continue Watching picks up the exact lecture you left off on, with the course grid and aggregate progress underneath.

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.

Outcome: It found an audience I did not plan for. 52 stars, 343 installer and portable downloads across seven releases, and a community-maintained Simplified Chinese translation someone forked and now keeps up on their own. The first six releases shipped in six days, because the bug reports came from people who were not me.

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
                    
First version — Two surfaces the OS never composites together

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.

1.0.x — One composited surface, gated before it was built

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.

2.0 — Make the video just another element

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.

Deskemy 2.0 player with the course contents panel open, subtitles over the video, and resources listed under their lectures
The 2.0 player. The controls, subtitles and course panel are ordinary Slint elements drawn over mpv's frames in the same OpenGL context. The panel lists each lecture's resources directly beneath it.

Engineering Highlights

1. Native playback: libmpv over FFI, not an HTML5 video tag

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.

2. Rewriting the interface without a flag day

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.

3. Shipping a rewrite to people who already have the old one

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.

4. Two-phase import so a big course never freezes the UI

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.

5. Rename-safe progress: identity from content, not path

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.

6. Searching the words that were actually spoken

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.

Deskemy search results across courses, sections, lectures, and subtitle text
Search spans courses, sections, and lectures, and optionally the subtitle text underneath them.
7. Two install modes, two data-residency guarantees

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.

Deskemy stats dashboard with watch-time streaks and an activity heatmap
Watch-time stats: an activity heatmap where hovering a day shows what was watched, streaks, and a daily goal.
Deskemy 2.0 player in a window with the control bar showing
The 2.0 player: filled controls, a thicker seek bar, and a darker shade behind them so they stay readable over bright video.
Deskemy 2.0 fullscreen player with the lecture title, clock, and subtitles
Fullscreen: lecture and section title, wall clock, "Ends at", and what plays next.
Deskemy course view with sections and ordered lectures
Course view: sections and ordered lectures with per-lecture progress.
Deskemy keyboard shortcuts cheat sheet
The shortcut cheat sheet, one ? 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.