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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

Artists, albums and tracks from your tags, with a play queue that follows you around the web app and gapless delivery between tracks.

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.

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.

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.

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.

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 →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.

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.

[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.1services:

kahawai:

image: iksteen/kahawai:0.0.20-rc.1

container_name: kahawai

restart: unless-stopped

devices:

- /dev/dri:/dev/dri # Intel / AMD; see below for NVIDIA

ports:

- "127.0.0.1:8420:8420"

- "8421:8421"

volumes:

- ./runtime/config:/config

- ./runtime/data:/data

- ./runtime/cache:/cache

- /srv/media:/media:ro--device=/dev/dri:/dev/dri--gpus all, with the NVIDIA Container Toolkit

Curious what your hardware can do? doctor lists every decode,

encode, remux and tone-mapping path it finds.

docker run --rm --device=/dev/dri:/dev/dri iksteen/kahawai:0.0.20-rc.1 doctorThis goes through a private control socket inside the container, so the public address never offers a way to claim your server.

docker exec -it kahawai kahawai hub init-admin

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.

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