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