English / 日本語
A split-tiling video / music player written with dotcl (Common Lisp on .NET), Avalonia and LibVLCSharp.
Split the window like Emacs, put a different video in each pane, and play them all at once — each pane keeps its own playlist, its own volume and its own position.
Screenshots: Blender Foundation open movies — Big Buck Bunny, Sintel, Tears of Steel and Caminandes 3: Llamigos, (CC) Blender Foundation, CC-BY 3.0.
Almost everything — the UI and the application logic — is written in Common Lisp (under src/, split by feature). The C# side handles boot plus the platform shims that need native callbacks. The same code runs as a GUI on Windows / macOS / Linux.
This project doubles as a technical demo of how far you can push a practical GUI app in dotcl. Read it as a worked example of "build an Avalonia
Windowfrom Common Lisp, drive a native media engine, and keep the platform-specific parts at arm's length."
- Emacs-style tiling of players. The window is a split tree (
src/tree.lisp);s/Ssplit the selected pane left-right / top-bottom,xcloses it,Tabmoves between panes, andShift+HJKLrearranges panes in place. Every leaf is a full player with its own playlist. - The GUI is written in Common Lisp — even the
Avalonia.Applicationsubclass is emitted from Lisp viadotnet:define-class(Playa.App). AssemblingWindow/Grid/VideoView, wiring events, posting to the Dispatcher — all on the Lisp side. - C# is only boot plus a platform shim.
Program.cspreloads the assemblies, loads the precompiled Lisp from a manifest and callsRUN-PLAYA. What remains in C# is the code that must hand a function pointer to the OS — the low-level mouse hook and DWM thumbnail on Windows, XI2 / XDND on X11,NSEvent/NSViewon macOS — because the libvlc video surface swallows the input Avalonia would otherwise deliver. - Lisp is ahead-of-time compiled at build time.
DotCL.Runtime's project-core build (<DotclProjectAsd>) compiles the ASDF system (src/playa.asd) to fasl in-process and puts the dependency fasls + manifest in the output. At runtime nothing is compiled and no sources ship. - Metadata follows the content, not the path. Scene bookmarks are keyed by a content id (size + head/tail hash, OpenSubtitles style), so renaming or moving a file keeps its marks (
src/meta.lisp).
- Split-tile layout with any number of panes, each playing independently; the split geometry is restored on the next launch
- Per-pane playlist — drag to reorder, repeat / shuffle, jump by cursor, with track lengths fetched in the background
- Scene bookmarks: mark a point or an A→B range, then jump between them; stored per content id so they survive a rename or move
- A-B repeat, per-pane volume, mute; pause-all / mute-all across every pane at once
- vi-style numeric prefixes —
3230gseeks to 32:30,30hrewinds 30 s,5ejumps to scene #5 - Recent files panel and menu, shared across panes
- Controls (transport strip, selection frame, cursor) auto-hide when idle and come back on any input
- Drag & drop files or folders onto a pane; dropping onto a specific pane loads it there
- Customizable key bindings in the settings window, persisted to
keymap.lisp - Windows: single instance (opening a file reuses the running window), per-user file association without admin rights, and a blur-free Alt+Tab / taskbar thumbnail via DWM iconic bitmaps
- macOS: native menu bar, Finder "Open With" / Dock drop, and a self-contained
.appthat bundles libvlc
Requires: .NET 10 SDK, plus libvlc for your platform.
| OS | libvlc |
|---|---|
| Windows | bundled automatically (VideoLAN.LibVLC.Windows) |
| macOS | supplied at runtime from the bundle, Homebrew (brew install vlc) or an installed VLC.app — no usable NuGet package exists for arm64 |
| Linux | the system's own (apt install vlc libvlc-dev or the distro equivalent) |
make build # or: dotnet build
make run # or: dotnet run
make run ARGS=/path/to/movie.mp4 # open a file at startup| Key | Action |
|---|---|
Space / Shift+Space |
Play / pause the selected pane — or every pane at once |
H L |
Seek back / forward (default 10 s, [count] = seconds) |
K J |
Volume up / down ([count] = that value) |
N / P |
Next / previous track |
M / Shift+M |
Mute the pane / mute everything |
[count]g |
Jump to a time — 3230g = 32:30 |
R |
A-B repeat |
B / Shift+B |
Mark a scene here / mark a scene range (start, then end) |
E / Shift+E / Control+B |
Next scene ([count]e = scene #n) / previous / delete nearest |
F |
Fullscreen |
O / Shift+O |
Playlist panel / recent-files panel |
Return / Delete |
In a list: play the selection / remove it |
Tab / Shift+Tab |
Next / previous pane |
S / Shift+S / X |
Split left-right / top-bottom / close pane |
Shift+HJKL |
Swap the selected pane with its neighbour (in a list: move the selected row) |
Control+O / Control+Shift+O |
Open file / open folder |
Q |
Quit |
Keys can be reassigned in the settings window; the full list is also under Help → Key list.
The Lisp side (src/, in load order — earlier files' definitions are visible to later ones):
package.lisp |
package definition |
util.lisp |
globals, logging, config directory + atomic s-expr I/O |
dnd.lisp |
drag & drop |
pane.lisp |
a pane: VideoView + MediaPlayer, its transport, context menu, construction |
playlist.lisp |
shared Playlist panel, repeat / shuffle / reorder |
recent-panel.lisp |
shared Recent Files panel |
meta.lisp |
per-content-id metadata and scene bookmarks |
tree.lisp |
the split tree and its rearrangement |
transport.lisp |
bottom transport bar for the selected pane |
dialog.lisp |
native file / folder pickers |
command.lisp |
command table, keymap, key dispatch for every input source |
config-io.lisp |
persisted recent files / prefs / session geometry |
assoc.lisp |
Windows file association (HKCU) |
about.lisp |
About and Key list dialogs |
settings-ui.lisp |
Settings window |
menu.lisp |
menu bar and macOS application menu |
app.lisp |
the Avalonia.Application subclass, main window, run-playa |
The C# side is boot plus the shims that need a native callback: Program.cs
(startup), NativeVideoInput*.cs (input over the video surface — Windows hook,
X11 XI2, macOS NSEvent), WindowThumbnail.cs (DWM Alt+Tab thumbnail),
Mixer.cs (per-pane volume), FileId.cs (content id), SingleInstance.cs.
Configuration lives in one place per user — %APPDATA%\playa\ on Windows,
~/.config/playa/ on Linux, ~/Library/Application Support/playa/ on macOS —
holding prefs.lisp, keymap.lisp, session.lisp, recent.lisp and meta/.
All of them are plain readable s-expressions, written atomically and read back
with *read-eval* disabled.
- dotcl (
DotCL.Runtime) — Common Lisp on .NET - Avalonia — cross-platform UI
- LibVLCSharp + libvlc — playback
- NAudio — per-pane volume mixing (Windows only)
make appimage # -> dist/playa-<version>-<arch>.AppImage
make tarball # -> dist/playa-<version>-<arch>.tar.gz (with install.sh)Both publish self-contained, so the .NET runtime comes along. libvlc does not:
VideoLAN publishes no usable Linux NuGet package, so playback uses the distro's own
VLC (apt install vlc). Both launchers check for it up front and name the package
to install — without that, a missing engine surfaces as
libvlc.so: cannot open shared object file, which says nothing useful. Set
PLAYA_SKIP_LIBVLC_CHECK=1 if your libvlc lives outside the ldconfig cache.
install.sh from the tarball installs under ~/.local/ (binary, .desktop entry
and icon) with no root access; uninstall.sh removes exactly those.
make win-installer # both x64 and arm64
make win-installer ARCH=arm64 # one architectureRequires Inno Setup 6: winget install --id JRSoftware.InnoSetup. Produces
dist/playa-setup-<arch>.exe — a per-user install under
%LOCALAPPDATA%\Programs\playa, so no administrator rights are needed. The .NET
runtime is bundled. File associations are not touched by the installer; register
them from Settings → File association, which writes only to HKCU.
Each installer carries libvlc for its own architecture only. The NuGet package ships x86, x64 and arm64 side by side (~280 MB together), which would otherwise triple the download for no benefit.
Releases carry Windows and Linux builds only — on macOS, build from source. The bundling step needs a VLC.app of the matching architecture, which the release runners cannot supply for both, and an untested bundle is worse than a build instruction. A macOS release will follow once one can be verified on real hardware.
make app # -> dist/playa.appPublishes self-contained (the .NET runtime is bundled, so a Finder launch does not depend on shell environment variables) and bundles libvlc + its plugins from an installed VLC.app.
MIT — see LICENSE.


