Kiosk-style video loop player with a clock overlay.
- UI: Avalonia (.NET 9)
- Playback: LibVLCSharp (VLC engine)
- Platforms: macOS, Windows, Linux
- .NET SDK 9 (from Microsoft, Homebrew on macOS, or your distro)
- VLC media player (64-bit). LibVLCSharp uses VLC's native libraries.
- Install 64-bit VLC from videolan.org (avoid the Microsoft Store version).
- Optional: set
LibVlcPathinPinscreen2.App/config.jsonto your VLC folder (the one withlibvlc.dll), e.g.C:\\Program Files\\VideoLAN\\VLC. If omitted, the app will look in common locations and on PATH. - Put videos in
Pinscreen2.App/videosor pick a folder in-app via "Set Media Folder…". - Run:
dotnet run --project Pinscreen2.App- Install VLC:
brew install --cask vlc(or from videolan.org). - Run via helper (sets required env vars for the dynamic loader):
./run-macos.sh- Or run manually (replace paths if VLC is elsewhere):
DYLD_LIBRARY_PATH=/Applications/VLC.app/Contents/MacOS/lib \
VLC_PLUGIN_PATH=/Applications/VLC.app/Contents/MacOS/plugins \
dotnet run --project Pinscreen2.App- Install VLC with your package manager (e.g.,
sudo apt install vlc). - Run:
dotnet run --project Pinscreen2.AppIf LibVLC cannot be found, set LibVlcPath in config to a directory that contains libvlc.so and a plugins directory (or ensure they are on the loader path).
Config file is stored per-user:
- Windows:
%LOCALAPPDATA%/Pinscreen2/config.json - macOS:
~/Library/Application Support/Pinscreen2/config.json - Linux:
~/.config/Pinscreen2/config.json
Default (OS-agnostic) contents:
{
"MediaFolders": [
"videos"
],
"ClockFormat": "HH:mm:ss",
"BalanceQueueByGame": true,
"LibVlcPath": ""
}- MediaFolders: Folders to scan (recursively). Relative paths resolve next to the app.
- ClockFormat: .NET time format string.
- BalanceQueueByGame: Interleave items by immediate parent folder.
- LibVlcPath: Optional override to VLC's library directory.
Other optional fields (saved by the app): ClockFontFamily, ClockColor, ClockXPercent, ClockYPercent, DelaySeconds.
Supported extensions: .mp4, .mov, .m4v, .mkv, .avi, .webm
Instead of scanning local folders, the app can pull videos from a Pinscreen2.Server instance running on another machine on your LAN. Files are downloaded into a local cache via an explicit Sync Now button (no streaming during playback), so the device works offline once synced.
The server (Pinscreen2.Server) exposes a manifest + file endpoints over HTTP.
- Publish it to a stable location on the source machine:
dotnet publish Pinscreen2.Server -c Release -r win-x64 --self-contained false -o D:\Pinball\Pinscreen2Server
- Drop a
server-config.jsonnext to the exe:{ "Root": "D:\\Pinball\\videos", "Port": 8088 } - Allow the port through Windows Firewall (one-time, admin PowerShell):
New-NetFirewallRule -DisplayName "Pinscreen2 Server" -Direction Inbound -Protocol TCP -LocalPort 8088 -Action Allow -Profile Private,Domain
- Optional auto-start on login: place a shortcut in the user's Startup folder pointing at a hidden launcher (
wscript.exe start-hidden.vbs) so the server runs without a console window.
Sanity check from another machine: http://<hostname>:8088/manifest.json should return a JSON list of every video.
In the app overlay, set Remote library URL to http://<hostname>:8088, hit Apply, then Sync Now. Sync diffs the manifest against the local cache, checks free disk space (with 1 GB headroom), and downloads anything missing — files that won't fit are skipped and reported. Future syncs only pull new files.
The cache directory defaults to %LOCALAPPDATA%/Pinscreen2/cache on Windows (and the equivalent on macOS/Linux); override via RemoteCacheDir in config.
A Claude Code skill at .claude/skills/pinball-video-curator/ automates curating videos extracted from pinball machines into D:\Pinball\videos\<Game Name>\. It probes each source file with ffprobe, applies an adaptive per-game aspect-ratio cluster filter (so multi-display games keep their main + sub displays and DMD-only games keep their narrow strip), drops short/blacklisted/junk files, and copies survivors into the library.
Symlink (or copy) the folder into your Claude config to use it:
New-Item -ItemType SymbolicLink -Path "$env:USERPROFILE\.claude\skills\pinball-video-curator" -Target "$(Resolve-Path .claude\skills\pinball-video-curator)"Then invoke via natural-language prompts like "curate pinball videos" — see .claude/skills/pinball-video-curator/SKILL.md for thresholds, action modes (--dry-run, --prune-dest, etc.), and the folder→game mapping.json.
scripts/debloat-kiosk.ps1 disables/uninstalls Windows components a kiosk display device doesn't need (telemetry, Search indexer, SysMain, Xbox, Print Spooler, Cortana, preinstalled UWP bloat, etc). Each section is opt-in via switches, with safer defaults on and riskier ones (Defender, OneDrive uninstall) gated behind explicit flags.
# Dry-run first (admin PowerShell on the pinscreen)
pwsh -File .\scripts\debloat-kiosk.ps1 -DryRun
# Apply the safe defaults
pwsh -File .\scripts\debloat-kiosk.ps1
# Aggressive: also disable Defender real-time + remove OneDrive
pwsh -File .\scripts\debloat-kiosk.ps1 -All-DisableDefender requires Tamper Protection to already be off in Windows Security or the changes silently revert.
dotnet buildNo trimming (safer for native deps like VLC):
- macOS (Apple Silicon):
dotnet publish Pinscreen2.App -c Release -r osx-arm64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=false- Windows:
dotnet publish Pinscreen2.App -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=false- Linux:
dotnet publish Pinscreen2.App -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=falseArtifacts are under Pinscreen2.App/bin/Release/<tfm>/<rid>/publish/.
The app pulls updates from this repo's GitHub Releases via the Check for Updates… button in the overlay. The release repo is hard-coded to davidvanderburgh/pinscreen-2.
.github/workflows/release.yml triggers on any pushed v* tag and publishes win-x64 / osx-arm64 / linux-x64 zips to the matching release. The release must already exist when the workflow runs (it uploads with gh release upload --clobber), so use:
gh release create vX.Y.Z --target main --title "vX.Y.Z" --notes "release notes here"That single command creates the tag, the release, and triggers the multi-platform build.
Prereqs: GitHub CLI (gh auth login), git remote points to GitHub.
- Tag a version
$ver = "v0.1.0"
git tag -a $ver -m "Pinscreen 2 $ver"
git push origin $verOr use the helper script:
./version.ps1 v0.1.0- Build and zip artifacts (Windows example; repeat for other platforms as needed)
./publish.ps1 win-x64 -Zip- Create the GitHub Release and upload assets
gh release create $ver .\Pinscreen2-win-x64.zip --title "Pinscreen 2 $ver" --notes "Release $ver"Naming tips:
- Use zip filenames containing the target runtime/OS, e.g.,
Pinscreen2-win-x64.zip,Pinscreen2-osx-arm64.zip,Pinscreen2-linux-x64.zip. - Each zip should contain the published app AND
Pinscreen2.Updater(.exe)in the same folder.
Updater behavior:
- The app calls
https://api.github.com/repos/{UpdateGitHubRepo}/releases/latest. - It picks a zip asset matching the current OS/architecture, downloads to a temp file, runs
Pinscreen2.Updaterto apply it, and relaunches.
- macOS: the dynamic loader must know VLC's library locations at process start. Use
./run-macos.shwhich setsDYLD_LIBRARY_PATHandVLC_PLUGIN_PATHbased on your VLC install orLibVlcPathinPinscreen2.App/config.json. - If you prefer manual run on macOS:
DYLD_LIBRARY_PATH=/Applications/VLC.app/Contents/MacOS/lib \
VLC_PLUGIN_PATH=/Applications/VLC.app/Contents/MacOS/plugins \
dotnet run --project Pinscreen2.App- If playback fails with "libvlc" not found or status shows "VLC: missing":
- Confirm 64-bit VLC is installed (Windows: avoid Store version; use Program Files, not Program Files (x86)).
- Set
LibVlcPathin config to the VLC folder with the library (libvlc.dll/libvlc.dylib/libvlc.so). - Alternatively, add VLC to your PATH and restart the app from the same shell.
- The queue rebuilds automatically when it reaches the end.
Prereqs:
- PowerShell execution policy allows running local scripts (recommended):
- Set for current user:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force
- Set for current user:
- GitHub CLI installed and authenticated:
gh --versionandgh auth login
Tags the repo, publishes for a single runtime, zips, and creates/updates the GitHub release.
Examples:
./version.ps1 v1.2.3 win-x64 "Release v1.2.3"
./version.ps1 v1.2.3 osx-arm64Publishes the app for a runtime; optional -Zip creates Pinscreen2-<rid>.zip.
Examples:
./publish.ps1 win-x64
./publish.ps1 win-x64 -ZipEnd-to-end release across multiple runtimes by invoking version.ps1 per RID.
Examples:
./release.ps1 v1.2.3 -Runtimes win-x64,osx-arm64,linux-x64 -Notes "Release v1.2.3"
./release.ps1 v1.2.3 -Runtimes win-x64