Documentation
Everything from first launch to running it as a service. If you just want it working, the quick start is five minutes; the rest is here for when you need it.
Quick start #
Before you begin
You need a DLNA or UPnP server somewhere on the same network. Jellyfin, Plex, Emby, MiniDLNA, BubbleUPnP and Windows Media Player all work, as does anything else that advertises itself over SSDP.
The Docker image and the desktop apps carry their own copy of ffmpeg. For the other two routes it is optional, and only affects transcoding. Without it everything else still works, and files the browser cannot decode will not play.
Docker
The shortest route for a NAS or an always-on Linux box.
docker run -d --name webdlna --network host -v webdlna-config:/config ghcr.io/alopexus/webdlna:latest
--network host is not optional if you want servers found automatically.
Discovery works by UDP multicast, and a container on Docker's default bridge network
never sees your LAN, so it finds nothing at all. Host networking is a Linux feature;
on Docker Desktop for macOS or Windows the "host" is a virtual machine rather than your
network. Bridge mode still works for everything else, including adding a DLNA server by
its rootDesc.xml URL.
Only /config holds anything you would miss. Mount your media read-only at
/media to use it as a folder source.
Debian, Ubuntu and Raspberry Pi
The installer fetches the .deb from the latest release, checks it against
the published checksums and hands it to apt, which pulls in Node and ffmpeg for you.
curl -fsSL https://raw.githubusercontent.com/Alopexus/WebDLNA/main/scripts/install.sh -o install.sh
less install.sh # it is short. read it before running it
sudo sh install.sh
WebDLNA then starts immediately and at boot, as a system account that cannot log in.
Settings live in /etc/webdlna/webdlna.conf, data in
/var/lib/webdlna, and journalctl -u webdlna -f shows the log.
Removing the package leaves your data alone, including apt purge.
You can also take the .deb straight from the
releases page and run
sudo apt install ./webdlna_*.deb. Debian bookworm ships Node 18, which is
the minimum; on anything older add NodeSource first.
Desktop app
Windows, macOS and Linux installers are on the releases page, with ffmpeg included. See installing as an app for how the tray behaves and why the first launch warns you.
From source
What you want if you intend to change it.
git clone https://github.com/Alopexus/WebDLNA.git
cd WebDLNA
npm install
Build once, then run the single production process:
npm run build # builds the frontend and the backend
npm start # serves both on port 3000
Then open http://<this-machine's-lan-ip>:3000 from any device on the
network. The backend serves the app from the same origin as the API, so there is nothing
else to configure.
Developing
For hacking on it, run the two dev servers with hot reload instead:
npm run dev # backend :3000 + frontend :8080
npm run dev:backend # backend only
npm run dev:frontend # frontend only
In this mode the app is at http://localhost:8080 and Vite proxies API calls
to the backend. npm run type-check, npm run lint and
npm test cover the workspace.
First run. Discovery starts on its own. If nothing appears within a few seconds, jump to troubleshooting. It is almost always multicast or a VPN.
Connecting to servers #
Automatic discovery
The backend broadcasts an SSDP search every few seconds and adds servers as they answer, so one that boots after you do still turns up without a refresh. Discovered servers appear on the Servers page, and the browser is told about each one over a WebSocket as it is found.
Connecting by IP
Plenty of networks drop multicast. Most corporate WiFi does, along with some mesh routers and any
setup where the server sits on a different subnet. Add the server by address instead:
its IP and port (commonly 8200 for MiniDLNA, 1900-derived
ports for others). WebDLNA will fetch the device description directly.
Known servers
Anything you connect to is remembered, so it is one click away next time even before discovery has found it again. Servers you no longer use can be removed from the list. Their reachability is re-checked in the background, so a server that has gone away is marked rather than silently failing when you click it.
Multiple servers are normal. Browse switches between them from the server dropdown, and search can query all of them at once.
Plex, Jellyfin, Emby and folders #
DLNA is one of five kinds of source WebDLNA can read. The others are Plex, Jellyfin, Emby, and any folder the machine running the backend can see. Add them from Settings, or from the Add source button on the Home page.
| Source | What it needs |
|---|---|
| Plex | The server address and an X-Plex-Token. |
| Jellyfin | The server address, then either a username and password or an API key. |
| Emby | The same as Jellyfin. They speak near enough the same dialect. |
| Folder | A path on the server. An SMB or NFS share mounted on that machine counts, which is the usual way to reach a NAS without running anything on it. |
| DLNA | Nothing, if discovery finds it. Otherwise the rootDesc.xml URL.
See connecting to servers. |
Credentials stay on the server. The browser gets a source id and never the token, so a Plex token cannot end up in your favourites, your history or an export.
What the media server already knows
Plex, Jellyfin and Emby track what you have watched, and WebDLNA reads that rather than guessing. Something part-watched there arrives here at the same position, and something finished there counts as finished. It works the other way too. Watch thirty minutes of a film in WebDLNA and Plex knows about it.
Whose viewing state. Jellyfin and Emby answer for the user account WebDLNA signs in as, and Plex answers for its token. Every profile using that source shares the account, so two people watching through one Plex login will overwrite each other there. That is a limit of those servers rather than something WebDLNA can work around.
Your library #
The Library page shows what you have, rather than what one particular server is offering. WebDLNA walks every source you have configured and builds one catalogue out of all of them, on six shelves. Movies, TV, Music and Photos, then Videos and Audio for everything it could not confidently place.
One film, however many copies
A film sitting on both the NAS and the Plex server is one entry with two copies behind it, not two entries that each half-remember where you got to. Which copy plays is yours to decide. Pin one for a particular film, or set an order for your sources under Settings and let everything follow it. A source whose last crawl failed drops down that order on its own, because a preferred copy you cannot fetch is worse than a second choice you can.
Where you got to belongs to the film rather than the copy, so starting on the NAS and finishing on Plex picks up where you left off. That holds even when you open the file from Browse, which knows nothing about the catalogue.
Finding things
- Sorting and filtering happen on the server, so a shelf holding eleven thousand things opens as fast as one holding ten.
- Filter by genre or year, and jump by first letter with the A to Z strip.
- Everything you can see is in the address bar. The back button goes back one step rather than out of the library, and a link to a shelf is a link someone else can open.
- Photos open in a lightbox with a slideshow and arrow keys, instead of the app trying to play a photograph as a two second video.
- Recently added is on the Home page, across every source at once.
Building the catalogue
Settings, then Library, then Catalogue. Refresh the index and watch it go. The crawl runs on the server, so closing the tab does not stop it, and progress appears live in whatever you have open. Eleven thousand items takes about fifteen seconds.
It declines to guess. "Holiday 2019.mp4" is not a 2019 feature film, so it lands on the Videos shelf instead of under Movies with a wrong year attached. Where a source already knows what something is, which means Plex, Jellyfin and Emby, WebDLNA takes its word over anything it could work out from a filename.
Browsing your library #
Browse mirrors the folder structure your server exposes, with breadcrumbs back up the tree. Most servers offer several views of the same library, by folder, by album, by genre, and all of them are here.
Grid or list
Grid gives you artwork at a glance; list gives you more rows and more metadata per row. The choice is remembered, and the default is set under Settings → Appearance.
Filtering and sorting
- Type. Everything, video, audio or images.
- Sort. The server's own order, alphabetical, or grouped by type.
- Search within the folder. Filters what is on screen instantly.
Item actions
- Play. Click the item.
- Favourite. The heart, which puts it on the Favourites page.
- Queue. Add to the end of the play queue.
- Add to playlist. Any playlist you have made.
- Download. Keep a copy for offline playback.
Anything you have partly watched carries a small progress badge, so you can see what you are part-way through without opening it.
Using the player #
Video plays in a custom control bar rather than the browser's default one, so the same controls appear everywhere and the seek bar can be honest about transcoded streams. Clicking the picture plays and pauses (mouse only, since on a touchscreen a tap reveals the controls instead, since they auto-hide).
Controls
- Seek. Drag the bar, or skip ten seconds with the arrow keys.
- Volume. Click the icon to mute; hover it for the slider.
- Speed. 0.25× to 2×.
- Subtitles. The CC menu, when tracks are loaded. See subtitles.
- Picture-in-Picture. Pops the video out into its own floating window.
- Fullscreen. The button, or F.
Resume
Position is saved as you watch, on pause, and when you leave the player. Come back and it offers to pick up where you were, or resumes automatically if you have set it to.
Short audio is deliberately excluded: anything under twenty minutes doesn't save a position, because a three-minute song has no use for one and having every track resume thirty seconds in is worse than useless. Podcasts, audiobooks, films and episodes all qualify.
The mini player
Navigating away while something is playing keeps it going in a compact player, so you can keep browsing without stopping the music. Expanding it returns to the full player at the same position.
Transcoding #
Browsers decode a narrow set of formats. Your library almost certainly contains files outside it. HEVC rips, AC-3 or DTS soundtracks, MKVs with codecs no browser ships. WebDLNA converts those on the fly.
When it kicks in
Each file is probed before playback, and the stream is sent straight through untouched whenever it is already playable, which is the common case and by far the cheapest. A transcode starts only when the probe finds something the browser can't handle:
- Video outside H.264, VP8/VP9, AV1 and a handful of legacy codecs.
- Audio outside AAC, MP3, Vorbis, Opus and FLAC, which includes AC-3 and E-AC-3, since Chromium and Firefox ship no Dolby decoder and the alternative is picture with silence.
- Containers browsers can't open.
How it plays
The encode is streamed as fragmented MP4 through MediaSource, so playback begins after the first fragment, a second or two, instead of waiting for the file to finish converting. Seeking outside what has been buffered restarts the encode at the new position; the player tracks the offset so the clock and the seek bar keep showing the real position in the film rather than the stream's own.
Hardware encoding
NVIDIA NVENC is detected and used automatically when present, with CUDA decode as well when that works, which keeps 4K sources feasible without pinning the CPU. Failing that it falls back to libx264 with a bitrate cap.
Quality and bandwidth
Settings → Playback caps the width of transcoded video: leave it automatic, or pin it to 1080p, 720p or 480p to spend less bandwidth on a phone screen or a slow link. Sources below the cap are never upscaled. Three transcodes run at once at most; beyond that, playback falls back to a direct stream.
ffmpeg must be on PATH for any of this. Without it,
incompatible files fail to play with a codec error. Set
WEBDLNA_TRANSCODE=0 to turn transcoding off even when ffmpeg is present.
Subtitles #
Upload .srt or .vtt files from the player. SubRip is converted
to WebVTT on upload, because the browser's <track> element accepts VTT
only and silently displays nothing when handed anything else. Several tracks can be
loaded at once and switched from the CC menu.
Cue timings are shifted to match a seeked transcode, so subtitles stay with the picture instead of captioning the opening scene over the middle of the film, and cues lift clear of the control bar while it is on screen.
Not yet. Subtitles embedded in the media file are not extracted, and casting does not carry subtitles to the Chromecast.
Queue & playlists #
The queue
Add anything to the queue from Browse, Search or Favourites, then reorder it, skip through it, or clear it. Shuffle uses a proper Fisher-Yates shuffle and keeps the original order so switching it off puts things back. Repeat covers off, all and one. When a track ends the next one starts by itself, honouring both settings.
The queue is a session: it isn't kept across a reload.
Playlists
Named playlists are yours to build. Create, describe, reorder, remove, play the lot. They are stored in the browser and can be exported to JSON and imported back, which is also how you move them between devices.
Smart playlists
Generated from what you have actually played, and refreshed as that changes:
- Most played. By play count.
- Recently added. Newest first.
- Random. A fresh shuffle each time.
- By artist, album or genre. Grouped from metadata.
Favourites, history and statistics
Favourites are a manual list, kept until you remove them. History records what you played and when, capped at a length you choose. Statistics turns both into something readable: play counts, total watch time, a breakdown by media type, which servers you actually use, and what you have been playing lately, exportable as JSON or CSV.
Search #
Ctrl/⌘ + K opens search from anywhere. The Search page adds filters for media type, year, genre and resolution, keeps your recent searches, and lets you save the ones you run often.
Search reads the catalogue, so it covers every source rather than only the DLNA servers, it does not need you to have visited a server first, and results come back immediately instead of after a crawl you wait through. Refresh the catalogue when your library has changed and search follows.
Duplicates. Results are de-duplicated by title and file size, but some servers, BubbleUPnP especially, present the same file through several virtual catalogues with different IDs and metadata, and a few of those slip through.
Offline downloads #
Downloading stores the file in the browser's own private storage (OPFS), and playback then works with no network at all. The server can be switched off, or you can be nowhere near it. Downloaded items are tagged Offline and get their own play button.
Managing space
- A cap you set, anywhere from 1 GB to 500 GB, with a live usage meter.
- What happens at the cap is your choice: ask (choose what to evict), least recently used (evict automatically), or block (refuse the download).
Browser storage is not permanent. Clearing site data removes downloads, and browsers may evict storage under pressure. Treat it as a convenience copy, never as your only one.
Casting #
The cast button in the player sends the current item to a Chromecast, with title, artist, album and artwork, and gives you play, pause, volume and stop from the browser. Sessions survive a page reload and are shared between tabs.
A Chromecast can't fetch localhost, so the backend works out this machine's
address on the network and hands the device that instead, which is why casting works
even when you are using the app on the same machine that runs it.
Requirements. A Chromium-based browser, and the Chromecast on the same network. Subtitles aren't carried to the device yet, and the receiver has to be able to decode the stream itself.
Profiles #
One installation can hold several profiles, each with its own favourites, history, playlists, statistics, resume positions and settings. Switch between them from the bottom of the sidebar.
Profiles and the positions inside them live on the server, so they follow you between devices. Start a film on the laptop, pick it up on the phone. Both keep working with the server unreachable and reconcile when it comes back, which matters because this is a progressive web app and watching something you downloaded should not need a network.
This is separation rather than security. Profiles keep a household's libraries from tangling together, but anyone at the browser can switch to any of them, and nothing about a profile is authenticated. For actual access control, see security.
Installing as an app #
WebDLNA is a progressive web app, so it installs to a phone home screen or a desktop dock and runs in its own window without browser chrome. When your browser offers it, a banner appears in the app; otherwise use Install or Add to Home Screen from the browser's own menu.
The app shell is cached, so it opens instantly and can start without the network, which is what makes offline downloads usable in the first place.
The desktop app
There are also real installers for Windows, macOS and Linux on the releases page. They carry their own ffmpeg, so MKV files and Dolby soundtracks play without you installing anything else.
It behaves as a tray application rather than a window. Closing the window leaves WebDLNA running and still serving every phone, tablet and television on your network, which is the entire point of it. The tray icon shows the address to type into those devices, copies it to the clipboard, and offers to start WebDLNA at login. Quitting is a deliberate act from that menu.
The builds are unsigned, so macOS Gatekeeper and Windows SmartScreen will both object the first time and you will have to allow it explicitly. Signing certificates cost money every year and that has not been taken on yet.
Settings reference #
Light or dark theme (a refresh applies it fully), an accent colour that repaints the whole interface, the logo included, the moment you pick it, and whether Browse opens as a grid or a list.
Resume behaviour, and the streaming quality cap that limits the width of transcoded video.
How many items history keeps, the offline library's size cap and eviction mode, and the per-server search indexes. Index, stop, re-index or clear, with statistics for each.
Whether discovery starts by itself, how often it re-scans, and Chromecast status with remote controls for whatever is currently casting.
Reset every preference to default (keeping favourites and history), or clear stored data for this profile entirely.
The keyboard reference, also reproduced below.
What this is, the version you are running, and who made it.
Keyboard shortcuts #
| Keys | Action |
|---|---|
| Space / K | Play or pause |
| ← / → | Seek back or forward ten seconds |
| ↑ / ↓ | Volume up or down |
| M | Mute or unmute |
| F | Fullscreen |
| Ctrl/⌘ + K | Search, from any page |
| Esc | Close a sidebar or dialog |
Player shortcuts apply while the player has focus, and are suppressed while you are typing in a field.
Running as a service #
For anything beyond trying it out, run WebDLNA as a background service so it starts at
boot and comes back after a crash. The Docker image and the
Debian package both do this for you, and are the easiest answer. The package installs a
systemd unit and a dedicated account that cannot log in, reading its settings from
/etc/webdlna/webdlna.conf.
If you installed from source instead, the repository's deploy/ directory has
ready-made configuration for PM2, systemd and launchd. Edit the paths to match where you
put it.
Environment variables
| Variable | Default | What it does |
|---|---|---|
PORT | 3000 |
Port the backend listens on. |
WEBDLNA_HOST | 0.0.0.0 |
Interface to bind. 127.0.0.1 makes it local-only. |
WEBDLNA_TOKEN | unset | Shared secret required by the API. See security. |
WEBDLNA_CONFIG_DIR | ~/.webdlna |
Where profiles, resume positions and the library index are kept. |
VITE_API_TOKEN | unset | Build-time token for the frontend. Only useful when you build it yourself, and it just saves entering the token once. See security. |
WEBDLNA_TRANSCODE | auto | Set to 0 to disable transcoding even when ffmpeg is installed. |
CORS_ORIGIN | unset | Extra allowed origins, comma-separated. |
Copy .env.example and fill in what you need. With the Debian package these
go in /etc/webdlna/webdlna.conf instead, and with Docker they are
-e flags or a compose environment: block.
Security #
WebDLNA assumes a trusted home network. By default the backend binds every interface and the API is open to anyone who can reach the port, which is the right default for a household and the wrong one for anything else.
Requiring a token
Set WEBDLNA_TOKEN to any secret and the server requires it.
openssl rand -hex 32 produces a reasonable one.
The first time you open the app it asks for that value and remembers it in that browser,
so you enter it once per device. Every /api/* request and WebSocket
connection has to present it after that, as an Authorization: Bearer header
or a ?token= parameter. /api/health stays open so uptime checks
keep working. Change the secret on the server and everyone is asked again.
If you build the frontend yourself you can set VITE_API_TOKEN to the same
value and skip the prompt. That is a convenience, not a requirement, and it is the only
thing that variable does now.
Know what the token is worth. It is one shared secret for the whole
server rather than an account per person, and it travels in the query string for media
URLs, because a <video> element cannot send headers. That is enough
to stop other devices on your LAN poking at the API. It is not authentication for the
open internet. For real isolation, bind to 127.0.0.1 and reach it through
a reverse proxy with its own login.
Don't put it on the internet
There is no user system, no rate limiting and no hardening for hostile traffic. If you want it away from home, use a VPN back to your network, Tailscale or WireGuard or whatever you already run, rather than a port forward.
Outbound requests are checked against an SSRF guard, so the media proxy can't be talked into fetching arbitrary internal addresses on a caller's behalf.
API reference #
Everything the frontend uses is a plain HTTP endpoint you can call yourself.
| Endpoint | Purpose |
|---|---|
GET /api/health | Liveness. Never requires a token. |
GET /api/ssdp/discover | Start discovery. |
GET /api/ssdp/devices | Servers found so far. |
POST /api/ssdp/stop | Stop discovery. |
POST /api/media/browse | Browse a container. Body: { deviceUrl, objectId }. |
GET /api/media/stream?url=… | Proxy a media stream. Supports range requests, force_transcode, format=fmp4, seek and max_width. |
GET /api/media/probe?url=… | Report codecs and whether a transcode is needed. |
GET /api/client-ip | This machine's LAN address, used for casting. |
WS /ws | Live discovery updates. |
Troubleshooting #
No servers found
- Turn off any VPN on the server device. This is the most common cause by a distance. A VPN on the phone or NAS breaks multicast on the local network.
- Check both machines are on the same network and subnet. Guest WiFi is usually isolated.
- Allow UDP port 1900 through the firewall on the machine running WebDLNA.
- Some networks drop multicast entirely. Add the server by IP address instead.
A file won't play
- Check ffmpeg is installed and on
PATH; without it, anything needing a transcode fails. - Video plays but there's no sound: usually AC-3 or DTS audio. With ffmpeg present this is transcoded automatically; without it, it can't be.
- Look at the browser console for the media error, and at the server log for what ffmpeg said.
Playback stutters or seeking is slow
- Transcoding is real work. Lower the quality cap under Settings → Playback.
- Seeking a transcoded stream restarts the encode from the new position, so a moment's pause after a long jump is expected.
- Only three transcodes run at once; a fourth falls back to a direct stream that may not play.
Indexing is slow
Indexing is one request per folder, and a large library is a lot of folders. Four run at a time to keep it civil for phone-hosted servers. Leave it going, because progress is saved as it works, and Stop keeps what it has found.
Casting doesn't appear
- Use a Chromium-based browser; the Cast SDK isn't available elsewhere.
- The Chromecast must be on the same network as the machine running WebDLNA.
Downloads vanished
Clearing site data removes them, and browsers can evict origin storage on their own when space runs short. Keep the original files.
Still stuck? The repository's README.md and CHANGELOG.md go
further into the internals, and the About tab in Settings tells you exactly which version
you are running.