When auto-advancing through a queue or playlist — or stepping with next/prev — the next video previously jumped to its saved resume timestamp. A freshly selected track now starts at the beginning, or at the A marker when an A-B loop is set for it. Resuming still applies when you reopen a single video directly. A-B markers set while playing from a playlist are now stored on that playlist's own copy of the video (entry.ab), so each playlist keeps its own loop and the markers sync to the database alongside the playlist. Non-playlist playback keeps using the global per-video marker map. Bumps service worker to v1.0.3 to bust the client cache.
YT Player
A lightweight, ad-free YouTube player with on-device playlists. No login, no
tracking, no ads — it extracts direct video/audio streams with yt-dlp and plays
them in a native window.
Built on zero-native (Zig + the system WebView) instead of Electron, so the binary is tiny — no bundled Chromium or Node runtime.
Features
- 🔍 Search YouTube without an account
- 🚫 No ads — plays the raw stream, never the YouTube player
- 📺 High quality via a dual-stream engine: a muted video track synced with a
separate audio track (1080p+) with no
ffmpegmuxing required; progressive formats are used as a fallback - 🎵 Audio-only mode — great for music, saves bandwidth
- 📂 On-device playlists — create, rename, delete, add/remove videos. Stored locally as JSON; nothing leaves your machine
- 💾 Offline cache / preload — a Save button and an Add to playlist button on the now-playing video download a self-contained copy into a permanent file cache, so it plays instantly and works offline. Videos added to a playlist are auto-preloaded and kept until you remove them.
- ⚙ Settings page — playback defaults (quality, volume, audio-only) plus cache management: see storage used, toggle auto-preload, and delete cached videos individually or all at once
- 🕘 Watch history
- ⏯ Full controls: seek, volume, playback speed, quality switching, fullscreen,
next/prev, and keyboard shortcuts (
space,←/→,f,m)
Architecture
The same frontend drives two native shells:
- Linux / macOS → zero-native (Zig + system WebView)
- Windows → Tauri (Rust + WebView2) — see Run on Windows
frontend/ Static web UI (no framework, no build step) — shared
index.html
styles.css
app.js Player engine + UI; auto-detects Tauri vs zero-native
src/ zero-native (Linux/macOS) shell
main.zig App definition + bridge handler registration
bridge.zig Native handlers: spawn yt-dlp, slim its JSON, local store
src-tauri/ Tauri (Windows) shell
src/main.rs Same handlers as Rust commands
tauri.conf.json Window, CSP, bundle (icons, yt-dlp resource)
Cargo.toml
scripts/
setup-ytdlp.js Downloads the standalone yt-dlp binary into ./bin
make-icon.js Generates appicon.png for `tauri icon`
bin/ yt-dlp lands here (gitignored)
app.zon zero-native manifest
The web UI talks to the native side over whichever bridge is present
(window.__TAURI__.core.invoke on Windows, window.zero.invoke elsewhere):
window.zero.invoke(...) |
Native handler | Returns |
|---|---|---|
yt.search { query } |
ytSearch |
{ ok, results:[…] } |
yt.streams { videoId } |
ytStreams |
{ ok, data:{ meta, audioUrl, qualities[] } } |
store.load {} |
storeLoad |
playlists / history / settings |
store.save { data } |
storeSave |
{ ok } |
cache.download { videoId } |
cache_download |
downloads a single-file copy into the offline cache |
cache.status { videoId } |
cache_status |
{ ok, cached, path?, size? } |
cache.list {} |
cache_list |
{ ok, items:[{id,size,path}], total } |
cache.delete { videoId } |
cache_delete |
removes one cached file |
cache.clear {} |
cache_clear |
removes all cached files |
The offline cache is implemented in the Tauri (Windows) shell. Files live in
<app_cache_dir>/videos/<videoId>.<ext>and persist until deleted from the Settings page. The frontend falls back to live streaming if a cached file is missing, and the cache calls degrade gracefully on shells that don't implement them.
Because the bridge is size-limited, the Zig handlers parse yt-dlp's large JSON
and return only the compact fields the UI needs.
Prerequisites
- Zig (0.14.x recommended) — https://ziglang.org/download/
- zero-native CLI:
npm install -g zero-native - Node.js (only to run the yt-dlp downloader script)
- No system
yt-dlpneeded — the setup script bundles it. (On Linux/macOS the bundled build is self-contained; Python is not required.)
Run on Windows (Tauri)
zero-native does not target Windows yet, so the Windows build uses a Tauri
shell (Rust + the WebView2 runtime that ships with Windows 10/11). The result is
a small standalone .exe/installer — no bundled Chromium or Node.
Build natively on the Windows machine (cross-compiling a WebView2 app from WSL/Linux is unreliable, so do this on Windows):
# Prerequisites (one time):
# • Rust https://rustup.rs (MSVC toolchain)
# • Microsoft C++ Build Tools (Desktop development with C++)
# • WebView2 runtime — preinstalled on Win11; on Win10 grab the Evergreen runtime
# • Node.js (to run the helper scripts + Tauri CLI)
npm install # installs the Tauri CLI (@tauri-apps/cli)
npm run setup # downloads yt-dlp.exe into .\bin
npm run make-icon # writes appicon.png
npm run tauri icon .\appicon.png # expands it into src-tauri\icons\*
npm run tauri:dev # hot dev window
npm run tauri:build # produces the installer (see below)
The installer lands in
src-tauri\target\release\bundle\ (nsis\*-setup.exe and msi\*.msi).
yt-dlp.exe is bundled as an app resource, so the installed app is self-contained.
One-shot release
scripts\release.ps1 does the whole flow — build, copy installers into
.\releases, then commit and push through WSL git:
pwsh -File scripts\release.ps1
To only commit + push from WSL (e.g. after building separately):
bash scripts/push.sh "your commit message"
Already in WSL and just want it running fast? Your WSL is WSLg-enabled, so you can instead build the Linux (zero-native) target and its window appears on your Windows desktop — see Setup & run. That needs WSL running each time; the Tauri build above is a true standalone Windows app.
Setup & run
Linux / macOS (zero-native). For Windows see Run on Windows.
# 1. Download the yt-dlp binary into ./bin
npm run setup # or: node scripts/setup-ytdlp.js
# 2. Generate the zero-native build files for your installed version
# (build.zig, build.zig.zon, src/runner.zig). Run this in a scratch dir and
# copy the generated build.zig / build.zig.zon next to this project, OR run
# init here and keep your files:
zero-native init ytplayer --frontend none
# 3. Merge: keep THIS repo's frontend/, src/main.zig, src/bridge.zig and app.zon.
# (src/main.zig shows exactly how the handlers are registered — fold that into
# the generated App if the scaffold differs.)
# 4. Build & launch the native window
zig build run
To refresh yt-dlp later (YouTube changes often): npm run update-ytdlp.
Notes & caveats
- zero-native is pre-1.0. The bridge handler signature used here follows its
documented contract (
fn(context, invocation, output) anyerror![]const u8). If your installed version exposes theInvocationtype orBridgeDispatcherfields slightly differently, only the wiring insrc/main.zigand thepayloadFieldhelper need adjusting — the handler logic is self-contained. - Stream URLs from
yt-dlpare IP-locked and expire after a few hours; the app re-fetches them each time you play a video, so this is transparent. - This is for personal use. Respect YouTube's Terms of Service and the rights of content creators.
License
MIT