Per-stream playback decisions
Direct play, remux, audio-only or full transcode, chosen stream by stream from capabilities the client measured. Among several copies of a film, the one that needs no transcode wins.
direct playremuxHLS
kah-hah-why · Hawaiian for stream; in te reo Māori, a strong fish of strong water
A self-hosted server for streaming your own movies, series, anime and music. It plays the file as it is whenever it can, and when it can't, it changes only the stream that's in the way.
pre-release Latest: v0.0.20-rc.1 MIT licensed Rust + GStreamer
What it is
Kahawai is a media server you run yourself, for media you own. Point it at folders of films, shows, anime and albums; it identifies them, fetches artwork and metadata, and streams them to a browser, an Android phone or a Google TV. Its web interface is compiled into the server binary, its state lives in SQLite, and it needs no external database and no account with anybody.
Your devices
Browserembedded web app
Android & Google TVnative client by tuxx
⇄⇅ /api/v1
The only thing clients talk to
hub users, libraries, metadata, watch state, playback decisions, direct play & remux
⇠⇡ dial in · mTLS
Satellites
mediahostindexes read-only media roots, serves bytes
mediahostthe NAS in the closet, the box at a friend's
transcoderwhatever has the GPU
Just one machine? The all-in-one mode runs the hub, a
mediahost and a transcoder in a single process, with no network hop between them.
That's the default in the container image, and it behaves exactly like the split
deployment does.
The cheapest path that plays
Every client tells the hub what it can actually decode, probed on the device itself and not looked up in a table of device models. The hub compares that with every stream of every copy of the file, and picks the cheapest plan that will work.
A film whose video your TV handles but whose DTS track it doesn't keeps its video untouched while only the audio is re-encoded. A different container is a remux, not a transcode. The GPU only gets involved when the picture itself has to change.
Direct playThe original bytes, with range requests end to end.
RemuxThe same streams in a container the client takes. Done by the hub.
Audio-only transcodeVideo copied, only the sound re-encoded. Still the hub.
Video transcodeOn a transcoder, hardware first, tone-mapped if HDR meets an SDR screen.
Every session reports what was actually done, not what was planned.
My collection is the messy kind: fansub anime with carefully typeset ASS subtitles and their own fonts, Blu-ray rips with image-based subtitles, HDR sources, and a lot of music. It lives on a low-power NAS, while the hardware that can actually transcode sits in a different machine.
The existing servers mostly cope. But "mostly" meant subtitles flattened to plain text, an entire film transcoded because one audio track didn't fit, a transcoder that had to sit on the same box as the disks, and anime matched by guessing at file names when a hash could say exactly what the file is.
So I started again, from a few rules I wasn't willing to bend: never touch the media files; decide from what was measured, never from what was assumed; do the least work that gives a correct result; and let storage and compute live wherever they already are.
Your media, played as it is, from wherever it lives.
What makes it different inside
Direct play, remux, audio-only or full transcode, chosen stream by stream from capabilities the client measured. Among several copies of a film, the one that needs no transcode wins.
direct playremuxHLS
Mediahosts and transcoders dial out to the hub, so they work behind NAT without port forwarding. One mediahost can serve several independent hubs, each seeing only the collections you publish to it.
mTLSNAT-friendlymulti-hub
VA-API, Quick Sync, NVENC, V4L2 and VideoToolbox, each dry-run before it's trusted. Jobs go to the machine that can do them in real time, not just the one that can do them at all. HDR is tone-mapped to SDR on the GPU.
IntelAMDNVIDIAApple
AniDB and AniList metadata, fansub naming conventions, absolute numbering, sequel and side-story relations, and ED2K hashes as the final word on what a file is. Sub-or-dub is remembered per user, per library.
AniDBAniListED2K
ASS/SSA keeps its typesetting and embedded fonts and renders in the browser. PGS and VobSub are drawn as overlays or OCR'd into text in any language Tesseract knows. Burn-in is the last resort, not the first.
ASS + fontsPGSOCR
Search OpenSubtitles from the player and see which results match your exact file. Downloads happen only when someone asks for one, and are kept on the hub, never written next to your media.
OpenSubtitlesuser-initiated
Each season is compared against itself in the background to find what repeats. Where nothing has been measured yet, viewers can opt in to TheIntroDB's community timings.
background analysisTheIntroDB
Every audio track is measured once against EBU R 128, so a re-encoded stream comes out at a consistent level instead of whatever gain the downmix happened to give it.
EBU R 128downmix
Artists, albums and tracks from your tags, with a play queue that follows you around the web app and gapless delivery between tracks.
gaplessqueue
Media roots are mounted read-only and never written to. Nothing is renamed, no artwork or NFO files are sprinkled around, and moving a file keeps its watch history.
read-onlycontent identity
One binary, SQLite state, online backup and restore. kahawai doctor
lists every decode, encode and tone-map path it found, and every playback session
can be downloaded as a single diagnostic bundle.
doctorbackupSQLite
The web app uses only the public, versioned API, so any client can do what it does. The hub publishes its full OpenAPI 3.2 contract and a Swagger UI.
OpenAPI 3.2SSE
Compared with Jellyfin, Plex and Emby
They're good, mature projects, and if one of them serves you well there's no reason to switch. Kahawai makes some different choices at the foundations.
Typical media server
kahawai~
Media engine
usuallyFFmpeg, driven by building command lines.
kahawaiGStreamer pipelines built from a typed plan, with FFmpeg's decoders inside it for the stubborn files.
Shape
usuallyOne server process holds the files, the database and the transcoder. Remote transcoding is an add-on.
kahawaiHub, mediahosts and transcoders are separate roles by design, on as many machines as you like, or all in one.
Deciding what to send
usuallyDevice profiles and whole-file transcodes when one thing doesn't fit.
kahawaiMeasured capabilities, decided per stream: the cheapest of direct play, remux, audio-only or video transcode.
Hardware acceleration
usuallyConfigured by hand; on Plex and Emby, part of a paid tier.
kahawaiProbed and dry-run at startup, ranked by measured speed, and free.
Anime
usuallyA plugin or a naming workaround on top of TV series.
kahawaiIts own media type: AniDB/AniList, fansub names, hash identity, dual audio, typeset subtitles.
Accounts
usuallySometimes a vendor account or cloud relay for sign-in and remote access.
kahawaiNo vendor account, no relay. Users live in your hub's own database.
Your media folders
usuallyOften allowed to write artwork, metadata or subtitles next to your files.
kahawaiRead-only, always. Everything Kahawai learns stays in its own state.
Where they're ahead, honestly: years of maturity, a large community, clients on almost every platform, plugins, and live TV and DVR, which Kahawai doesn't do at all. Kahawai is pre-release software, and it isn't trying to be a drop-in replacement for any of them.
How to get it
The container image is the supported way to run Kahawai. It bundles a pinned GStreamer stack with Kahawai's own media fixes, every hardware driver it knows how to use, and OCR models for every language.
iksteen/kahawai on Docker Hub, for amd64 and arm64.
Release notes, source archives and bare Linux binaries for debugging.
GitHub releases → macOSFor a Mac that should transcode with VideoToolbox, which a container can't reach.
macOS instructions → Arch Linux
kahawai with yay or paru, as a systemd service. Packaged separately, so
it may lag behind the latest release.
A native Kotlin, Compose and Media3 client for phones, tablets and TVs. A separate project developed by tuxx, built on the hub's public API.
kahawai-android →Heads up.
Kahawai is in active development and only ships release candidates so far. Expect
breaking changes between versions, and keep a backup of your hub
(kahawai hub backup) before upgrading.
How to install it
One container running the hub, a mediahost and a transcoder. This is the quickest way to a working server; you can split the roles across machines later without losing users, libraries or watch history.
Three persistent directories: configuration, data and cache.
shell
mkdir -p runtime/config/kahawai runtime/data runtime/cache
Save this as runtime/config/kahawai/kahawai.toml. Each collection
is a media type (movies, series, anime or
music) and the folders it lives in, as seen from inside the
container.
kahawai.toml
[all_in_one]
transcoder = true
[hub]
bind = "0.0.0.0:8420"
satellite_bind = "0.0.0.0:8421"
[mediahost]
name = "local"
[[mediahost.collections]]
name = "movies"
media_type = "movies"
roots = ["/media/movies"]
[[mediahost.collections]]
name = "series"
media_type = "series"
roots = ["/media/series"]
Replace /srv/media with wherever your media is. It's mounted
read-only. Port 8421 is only needed if you'll attach satellites later.
docker run -d \
--name kahawai \
--restart unless-stopped \
--device=/dev/dri:/dev/dri \
-p 127.0.0.1:8420:8420 \
-p 8421:8421 \
-v "$PWD/runtime/config:/config" \
-v "$PWD/runtime/data:/data" \
-v "$PWD/runtime/cache:/cache" \
-v /srv/media:/media:ro \
iksteen/kahawai:0.0.20-rc.1
Intel or AMD--device=/dev/dri:/dev/dri
NVIDIA--gpus all, with the NVIDIA Container Toolkit
No GPUleave the device out; it encodes in software
Curious what your hardware can do? doctor lists every decode,
encode, remux and tone-mapping path it finds.
shell
docker run --rm --device=/dev/dri:/dev/dri iksteen/kahawai:0.0.20-rc.1 doctor
This goes through a private control socket inside the container, so the public address never offers a way to claim your server.
shell
docker exec -it kahawai kahawai hub init-admin
Go to http://localhost:8420, sign in, and build your libraries from the collections it found. Before you let anything outside the machine reach it, put a TLS reverse proxy in front.
The kahawai package builds
Kahawai and its own patched GStreamer from source, kept apart from Arch's, and runs
it as a systemd service under a kahawai user. Add your collections to
/etc/kahawai/kahawai.toml, which explains how, then start it. The AUR
packages are updated separately from the releases, so they may lag behind the latest
one.
shell
yay -S kahawai # or: paru -S kahawai
sudoedit /etc/kahawai/kahawai.toml
sudo systemctl enable --now kahawai
sudo -u kahawai kahawai --config /etc/kahawai/kahawai.toml hub init-admin