Three components, three paths
Tenjin consists of the media server, the optional relay server and the clients, all in C++ with Qt. A client reaches a server over one of three paths; the relay is only needed for B and C. Any number of servers can register with one relay, and a server with several relays.
| Path | When | Encryption | Relay sees contents |
|---|---|---|---|
| A · Direct | LAN, server reachable from outside (port forwarding, UPnP) or successful hole punching | QUIC/TLS 1.3, end-to-end | no |
| B · Relay passthrough (default) | Direct path fails | QUIC/TLS 1.3 end-to-end, the relay only forwards (TURN principle) | no |
| C · Relay termination | Enabled by the relay admin, e.g. for a cache | TLS client ↔ relay and relay ↔ server, sign-in with a signed token | yes |
The client tries path A first (local addresses, then public ones) while hole punching starts in parallel via the relay. If nothing succeeds within a window of about three seconds, it falls back to B or C.
Transport
- QUIC (HTTP/3) with TLS 1.3 for control and streaming, libraries ngtcp2 and nghttp3, TLS via OpenSSL 3.5 or newer; fallback to TCP with TLS 1.3 (HTTP/2) if UDP is blocked. The relay does not terminate QUIC and gets by with OpenSSL 3.0.
- Packet size: UDP payload 1,350 bytes by default so VPN and tunnel headers fit; the administrator can switch to dynamic path MTU discovery up to 1,452 bytes or a fixed size between 1,200 and 1,452 bytes.
- NAT: address candidates are brokered ICE-style via the relay; against NATs that assign a new port per destination there is a port search with many client sockets.
- Network change: the client detects a change by its addresses, drops the QUIC connection and rebuilds the path (direct, hole punching, relay). A silently broken path counts as dead after eight seconds without a packet.
Media server modules
The media server is a background service made of modules that communicate through interfaces and can be tested individually.
| Module | Task |
|---|---|
| Core / service host | Start as Windows service, daemon or container, configuration, logging |
| Net | QUIC/HTTP-3 server, API routing, relay control channel, mDNS |
| Auth | Accounts, permissions, tokens, devices, account linking |
| Storage | Uniform file system abstraction over local and remote sources |
| Library | Scanner, folder structure recognition, mapping to metadata |
| Metadata | Provider chain, rate limiting, image cache, search |
| Analysis | Preview images, chapters, intro/outro, loudness |
| Streaming | Playback decision, HLS packaging, transcode sessions |
| Scheduler | Scheduled and manual tasks with progress |
| Sync | Synchronisation of favourites and progress via the relay |
| Plugins | Loading, versioning, isolating extensions |
| Telemetry | Activities, load metrics, stream details for administration |
Concurrency and processes: separate thread pools for I/O, CPU work and network; SQLite in WAL mode with exactly one writer thread and several read connections. Everything that decodes media files runs in separate processes (media worker, transcoding), so a broken file or a driver bug never takes the server down.
Storage sources
Contents live locally or on shares in your own network; cloud storage is not supported. Polling only compares directory listings and never reads file contents; the interval adapts.
| Source | Access | Change detection |
|---|---|---|
| Local | direct | Real time: ReadDirectoryChangesW, inotify, FSEvents |
| SMB | UNC path, libsmb2 or mount | SMB2 change notify, otherwise polling |
| NFS | Mount or libnfs | Polling |
| WebDAV | HTTP(S) | PROPFIND with ETag and Last-Modified |
Playback decision
Direct play. The file is streamed unchanged if container, codecs and tracks match the device's capabilities and the bitrate stays below the effective limit.
Remux. Only the container is repackaged, video and audio stay untouched; for instance when the device supports the codec but not the container.
Transcode. Only when necessary: re-encode the video or audio track, with hardware acceleration (NVIDIA, Intel, AMD) and selectable quality levels.
The effective limit is the minimum of device capability, user limit, device limit and client setting; the upload balancer distributes internet bandwidth across external streams.
Metadata
Per media type a global order of providers applies (TMDB, OMDb, …); if the first finds nothing, the next is asked. Rate limits apply per provider instead of globally. Texts and IDs live in SQLite, images either in the metadata folder or next to the media file following the Kodi/Jellyfin convention (poster.jpg, fanart.jpg, season01-poster.jpg), so other programs find them too. Existing trickplay images from Jellyfin are reused directly.
Client
client-core· Qt-free logic: connection setup, session, playback decision, modelsViewModels· state and actions of the pages, without Qt QuickQML per form factor· desktop (sidebar), tablet (navigation rail), phone (bottom bar, landscape), TV (ten-foot, focus)Player· libmpv (desktop), Media3/ExoPlayer (Android), AVPlayer (Apple)local proxy· the player fetches the stream through a local proxy of the client that handles TLS and tokens
One client program covers all form factors; the image cache stores only versioned addresses (?v=<updated_at>), which the server delivers as immutable.
Operation
- Windows 10/11 and Windows Server: media server as a service; SMB sources as UNC paths with stored credentials, because services do not see drive letters.
- Linux: primarily as Docker containers (
tenjin-server,tenjin-relay, based on Debian 13), alternatively systemd; OpenSSL 3.5 or newer is required for server and app. - macOS and QNAP (QTS): planned as agent and daemon respectively.
- Clients: Windows, macOS, Linux; Android 9 and newer for phone, tablet, Android TV; Fire TV with a 32-bit package.
Test strategy
Unit
Core logic in UI-free libraries with unit tests; Qt Quick only for presentation.
Integration
Server, relay and client working together, import from Plex and Jellyfin with a dry run.
UI tests
265 user interface tests (ctest -L ui) across all form factors, with a preview harness for desktop, phone, landscape and TV.
Devices
Nvidia Shield, Fire TV Cube and phones on the network; stutter is measured with dumpsys SurfaceFlinger.
