photon
A calm, minimal desktop media player built exclusively for Jellyfin — direct play when possible, silent transcode when it isn't, mpv handoff when needed.
photon is a desktop media player built for one thing: watching a Jellyfin library without friction.
Photon is not a media manager. It is a media player.
Why I built this#
Jellyfin’s web client is fine, but it’s a web app first: media management, admin panels, plugin surfaces, all bundled into one interface. I wanted something that opens straight to Continue Watching and Recently Added, and gets out of the way the moment I pick something.
How it works#
Sign in to one Jellyfin server; the session persists in the OS keychain. From there it’s Home (Continue Watching, Recently Added Movies, Recently Added Shows — nothing else), a merged Movies/Shows grid, and instant local search that falls back to server-side search for episodes.
Playback tries three paths in order:
- Direct play via HTML5/HLS if Chromium can decode the source natively
- Silent server transcode through Jellyfin if it can’t
- Handoff to a local
mpvprocess for direct play with zero transcoding, when the user wants guaranteed quality over convenience
Watch progress syncs back to the server either way, so switching between Photon and any other Jellyfin client stays consistent.
Technical challenges#
Three playback paths, one UI#
The player has to decide, per file, whether Chromium’s media pipeline can handle the codec/container combination, whether to let Jellyfin transcode, or whether to shell out to mpv. Whichever it picks, it still has to present a single consistent set of controls (audio/subtitle tracks, delay, styling, PiP, speed) regardless of which backend is actually rendering the frame.
Packaging without a paid Apple cert#
The macOS build is ad-hoc signed, not notarized, since notarizing requires a paid Apple Developer account. Gatekeeper flags any downloaded app in that state as “damaged,” which is a false positive rather than a corrupt download. The fix is a one-time xattr -cr on the app bundle, documented directly in the README instead of pretending the friction doesn’t exist.
Stack#
- Electron + electron-vite for the desktop shell
- React + TypeScript for the UI
- HTML5/HLS for native playback, mpv as an external process for direct-play fallback
- Jellyfin API for library data, search, and playback-state sync
- Conventional Commits + commitlint + husky, with CI-enforced PR checks
What this proves#
Photon is scoped deliberately narrow — one server, one purpose, no library management. The complexity that’s there is all in making that narrow scope feel effortless: keychain-backed sessions, three playback strategies negotiated transparently, and packaging that’s honest about its own limitations.