zond/xtremio

A stremio client with slightly different stream server

Dart

0

928 commits

updated Sep 17, 2026

See the code

README

Xtremio

xtremio

A native, cross-platform Stremio client built on a Rust core with a Flutter UI.

CI source: MIT binaries: GPL-3.0-or-later

Xtremio is the client half of a two-part project. The other half is zond/stream-server, a headless torrent-streaming server written in Rust, which Xtremio embeds in its own process. It pairs that with stremio-core -- the official Rust engine for addons, catalogs, library and playback state, built here from a fork -- and media_kit/libmpv for playback.

What it does

All of this is built and runs today. docs/STATUS.md is the screen-by-screen inventory; a feature with a design document links it.

  • Catalogs and search across every addon installed, and a library. A board of continue-watching and a row per catalog that answered, discover over the engine's own filters, a search that asks every addon supporting it -- and on the board, in search and under a title's sources, a line naming the addons that could not answer, so a dead addon is never mistaken for a title nobody has.
  • Torrent streaming with no external binary. stream-server runs in-process on loopback: nothing to ship beside the app, launch, or keep alive on mobile. In the default layout a title's sources are one row per release rather than one per addon offering it, in a section per resolution, ranked by peers per megabyte unless another order is picked; a torrent starts behind a card that says what it is doing -- checking, finding peers, buffering -- instead of a spinner.
  • Offline downloads. A download is a file pinned in the embedded server: it is kept, piece by piece, in the one torrent-data root the streaming cache uses, and never exists as a whole file. A finished download plays through the same in-process server off the pieces already on the device -- no peer, no tracker, no network -- and only once the server answers that it holds the file whole. On Android a foreground service keeps one going after the app is left.
  • A player rather than a video widget. Buffered seek bar, keyboard and remote shortcuts, audio tracks, embedded and addon subtitles, a stats OSD reporting hwdec, the swarm, what this device holds of the stream either side of the playhead and what a torrent has committed and moved since it went live, and an up-next countdown that hands over to the next episode.
  • Subtitle timing that is nudged or measured, and then remembered. Shift the lines by hand from a panel that survives the controls fading, mark a line where it belongs and let two marks give the rate, or have the drift measured against another subtitle the viewer says is in sync -- which is where a stretch comes from, since nothing here presses a multiplier and no declared frame rate decides one. What was fixed is stored against the series and the subtitle's release group (a shift against the video release too), so the next episode starts right (docs/ARCHITECTURE.md).
  • Android TV and Google TV as their own layout, not a phone app on a big screen: D-pad traversal with a focus memory per tab, a focus ring built to read over unknown poster art, remote keys in the player, ten-foot density and overscan. Run on a physical Chromecast with Google TV (ANDROID.md).
  • Casting to a Chromecast from an Android phone, where the receiver can decode what the embedded server is already serving. The bytes go over the LAN untouched, from a second listener on the server that exists only while a cast session does and serves only torrents and archives the app has already opened -- no control routes, no /proxy. The player screen becomes a remote, and a cast does not binge: the end of an episode on the television never starts the next one. What it refuses, why it refuses rather than guesses, and the fact that no real receiver has confirmed it yet are in docs/CASTING.md.
  • Sharing you can see and stop. Share while idle (Settings, on by default) keeps uploading to other peers when nothing is playing; off, the server chokes every peer until a player reads from it again. A status light on the main screens is lit only while the server measures bytes moving to or from peers with nothing playing, never because of the setting: an arrow up for uploading, down for bytes coming in (an offline download filling in, or the title played last fetching what it keeps). Pressed while it shows an upload, it offers Not now (until the next start) or Stop sharing; while it shows bytes coming in, a Cancel for each offline download on its way.
  • Addons installed from the web. An addon site's Install button hands the OS a stremio:// link; where Xtremio can register that scheme it opens that addon's details screen, and nothing is installed until the button waiting there is pressed. The contract in full, and the registration per platform, is in docs/DEEP_LINKS.md.

Getting it

Every version tag builds Linux, Windows, macOS and both Android ABIs and attaches them to a GitHub Release -- that is where a build comes from. Nothing is tagged yet, so until the first one that page is empty and building it yourself is the only way. Two things about those builds are worth knowing before installing, and the release notes say both: the APKs are signed with the Flutter template's debug key, and the macOS build is unsigned.

flutter pub get
make run DEVICE=linux   # flutter run -d linux, stamped with version and commit
make linux              # a release build; also apk, apk-tv, apk-split, macos, ios (compile check)

The Makefile adds two --dart-defines so the Diagnostics screen can say which build it is, and its APK targets also set the ABI and a version code that Android installs over an APK from make apk-split. Plain flutter run -d <device> works too and reports app: unknown. A build needs Flutter stable (CI uses 3.47.1) and a Rust toolchain no older than rust-version in rust/Cargo.toml (1.97.1): the Rust crate is compiled by the build itself, through cargokit. Linux desktop also needs clang, cmake, ninja, pkg-config, GTK 3 dev libraries, and libmpv-dev (media_kit links libmpv); Android has a document of its own, ANDROID.md. Everything else a dev machine wants is in docs/OPERATIONS.md.

How it works

┌──────────────────────────────────────────────────────────────┐
│  Flutter UI (this repo) — screens, navigation, playback UI   │
├──────────────────────────────────────────────────────────────┤
│  Dart ⇄ Rust FFI (flutter_rust_bridge), crate in rust/       │
│   • stremio-core   → addons, catalogs, search, library,      │
│                      account, playback state (the "brain")   │
│   • stream-server  → embedded: settings, stats, storage and  │
│                      downloads as FFI calls                  │
├──────────────────────────────────────────────────────────────┤
│  media_kit / libmpv — fetches the media over loopback HTTP,  │
│  decodes and renders it (direct play; codecs and subtitles   │
│  on-device)                                                  │
└──────────────────────────────────────────────────────────────┘

The UI stays thin: discovery/library/addon logic lives in stremio-core, the bytes come from stream-server, and the client's job is presentation plus driving libmpv. stream-server runs in-process: the Rust crate in rust/ links it as a library and starts it on its own thread with its own runtime, bound to 127.0.0.1 on a port the OS picks and retargeting stremio-core at the address it reads back, so no sidecar binary ships and no fixed port is lost to a desktop Stremio. The Dart side never speaks HTTP to it: libmpv fetches the media routes, the app's own questions -- settings, a torrent's stats, storage, downloads -- are FFI calls into the server's library API, and stremio-core's requests to it carry a per-launch bearer token that only the Rust side holds. The only HTTP it serves beyond loopback is the media listener a cast session turns on and off. Because a capable on-device player handles codecs and subtitles, the server never transcodes -- it just gets bytes onto an HTTP connection. Settings can point stremio-core at a remote streaming server by URL, and torrents then play straight off that one; the embedded server still starts and still fronts an addon's direct links. The embedded one is the default.

How that bridge is built, what crosses it and what every field of the state means is in docs/ARCHITECTURE.md.

Every git dependency in rust/Cargo.toml is pinned to a rev with its reason beside it: the three forks are listed in docs/ARCHITECTURE.md.

Platform support

The hard constraint is BitTorrent: the streaming path needs raw TCP/UDP sockets, a local HTTP server, disk cache, and libmpv. That decides everything.

PlatformSupportNotes
Linux (desktop)✅ First-classThe easiest target; video is software-rendered until media_kit's Linux renderer lands (docs/OPERATIONS.md).
Windows (desktop)✅ Built weekly in CI (build.yml)Flutter desktop, media_kit and native Rust, as on Linux; registering stremio:// needs an installer and there is none (docs/DEEP_LINKS.md).
macOS (desktop)✅ Built weekly in CI (build.yml)Native Rust + media_kit; unsigned, and needs a Mac to build yourself -- there is none in the project.
Android✅ SupportedRust cross-compiles to the NDK and is embedded as a native lib; the primary mobile target (ANDROID.md).
Android TV / Google TV✅ SupportedThe same app, not a separate build; install the APK for the ABI the box reports -- a Chromecast with Google TV is 32-bit, make apk-tv (ANDROID.md).
iOS❌ Does not build todayCI compiles it and it fails in an upstream crate (librqbit-dualstack-sockets 0.7.0 calls a socket2 method iOS does not have). Past that, there is no signing identity here, the App Store is out on GPL-3 (see License), and iOS throttles background work.
Web❌ Not possibleA browser cannot do BitTorrent -- no raw sockets, no local server, no libmpv. A thin client onto a separate server is a different architecture, not this app.

What is next

What is genuinely not built:

  • Cloud storage sources (e.g. Google Drive) -- most naturally via a Stremio addon that resolves cloud files to playable URLs; the provider's OAuth or API-key setup is the fiddly part.
  • Media3 remuxing for casting, to let a receiver play a stream it cannot decode as it stands. It would run on the sending device with its platform hardware codec (Android MediaCodec first) -- never ffmpeg, never software transcoding in the Rust core. Until then such a stream is refused rather than mangled.

What is written down where

DocumentWhat is in it
docs/STATUS.mdWhat is built today, screen by screen.
docs/ARCHITECTURE.mdHow the Rust core is wired in: the bridge, what crosses it as JSON, every model field, the pinned forks, and what the app reads from the settings. Its design notes are docs/phase3-design.md.
docs/OPERATIONS.mdWhat to run before a commit, how to see video play, what the server's storage costs, and getting a log off a device.
ANDROID.mdBuilding, running and verifying on Android and Android TV: prerequisites, the APK, the manifest decisions, the emulators, a real box.
docs/CASTING.mdThe cast button: what it hands a receiver untouched, and every rule it refuses on.
docs/ADDONS.mdHow each installed addon has been answering, and the verdict the Installed tab reads off that record.
docs/DEEP_LINKS.mdWhat a stremio:// link may and may not do, and how the scheme is registered on each platform.
AGENTS.mdWhat a change has to satisfy here; see Contributing.

Contributing

AGENTS.md is what a change has to satisfy here: single-concept commits, the verification that gates them, and the rules a real television taught us. Read it before opening a pull request. CI runs the same checks on every push to main and every pull request: formatting, analysis, the Flutter and Rust suites, cargo clippy -D warnings, a cargo check of the core for 32-bit Android, and a check that the flutter_rust_bridge bindings regenerate to what is committed (AGENTS.md has the commands).

License

The source here is MIT (LICENSE). A compiled Xtremio embeds stream-server, which links unrar-rs (GPL-3.0-or-later) so a RAR archive in a torrent plays: distributed binaries are GPL-3.0-or-later (LICENSE-GPL-3.0). unrar-rs asks that a binary reproduce its licence file, the unRAR restriction included, so that text ships in the app too (LICENSE-unrar-rs; Settings → About → Open source licences). stream-server with default-features = false drops both.

Contributors

zond

928 commits

zond/xtremio

A stremio client with slightly different stream server

Dart

0

928 commits

updated Sep 17, 2026

See the code

README

Xtremio

xtremio

A native, cross-platform Stremio client built on a Rust core with a Flutter UI.

CI source: MIT binaries: GPL-3.0-or-later

Xtremio is the client half of a two-part project. The other half is zond/stream-server, a headless torrent-streaming server written in Rust, which Xtremio embeds in its own process. It pairs that with stremio-core -- the official Rust engine for addons, catalogs, library and playback state, built here from a fork -- and media_kit/libmpv for playback.

What it does

All of this is built and runs today. docs/STATUS.md is the screen-by-screen inventory; a feature with a design document links it.

  • Catalogs and search across every addon installed, and a library. A board of continue-watching and a row per catalog that answered, discover over the engine's own filters, a search that asks every addon supporting it -- and on the board, in search and under a title's sources, a line naming the addons that could not answer, so a dead addon is never mistaken for a title nobody has.
  • Torrent streaming with no external binary. stream-server runs in-process on loopback: nothing to ship beside the app, launch, or keep alive on mobile. In the default layout a title's sources are one row per release rather than one per addon offering it, in a section per resolution, ranked by peers per megabyte unless another order is picked; a torrent starts behind a card that says what it is doing -- checking, finding peers, buffering -- instead of a spinner.
  • Offline downloads. A download is a file pinned in the embedded server: it is kept, piece by piece, in the one torrent-data root the streaming cache uses, and never exists as a whole file. A finished download plays through the same in-process server off the pieces already on the device -- no peer, no tracker, no network -- and only once the server answers that it holds the file whole. On Android a foreground service keeps one going after the app is left.
  • A player rather than a video widget. Buffered seek bar, keyboard and remote shortcuts, audio tracks, embedded and addon subtitles, a stats OSD reporting hwdec, the swarm, what this device holds of the stream either side of the playhead and what a torrent has committed and moved since it went live, and an up-next countdown that hands over to the next episode.
  • Subtitle timing that is nudged or measured, and then remembered. Shift the lines by hand from a panel that survives the controls fading, mark a line where it belongs and let two marks give the rate, or have the drift measured against another subtitle the viewer says is in sync -- which is where a stretch comes from, since nothing here presses a multiplier and no declared frame rate decides one. What was fixed is stored against the series and the subtitle's release group (a shift against the video release too), so the next episode starts right (docs/ARCHITECTURE.md).
  • Android TV and Google TV as their own layout, not a phone app on a big screen: D-pad traversal with a focus memory per tab, a focus ring built to read over unknown poster art, remote keys in the player, ten-foot density and overscan. Run on a physical Chromecast with Google TV (ANDROID.md).
  • Casting to a Chromecast from an Android phone, where the receiver can decode what the embedded server is already serving. The bytes go over the LAN untouched, from a second listener on the server that exists only while a cast session does and serves only torrents and archives the app has already opened -- no control routes, no /proxy. The player screen becomes a remote, and a cast does not binge: the end of an episode on the television never starts the next one. What it refuses, why it refuses rather than guesses, and the fact that no real receiver has confirmed it yet are in docs/CASTING.md.
  • Sharing you can see and stop. Share while idle (Settings, on by default) keeps uploading to other peers when nothing is playing; off, the server chokes every peer until a player reads from it again. A status light on the main screens is lit only while the server measures bytes moving to or from peers with nothing playing, never because of the setting: an arrow up for uploading, down for bytes coming in (an offline download filling in, or the title played last fetching what it keeps). Pressed while it shows an upload, it offers Not now (until the next start) or Stop sharing; while it shows bytes coming in, a Cancel for each offline download on its way.
  • Addons installed from the web. An addon site's Install button hands the OS a stremio:// link; where Xtremio can register that scheme it opens that addon's details screen, and nothing is installed until the button waiting there is pressed. The contract in full, and the registration per platform, is in docs/DEEP_LINKS.md.

Getting it

Every version tag builds Linux, Windows, macOS and both Android ABIs and attaches them to a GitHub Release -- that is where a build comes from. Nothing is tagged yet, so until the first one that page is empty and building it yourself is the only way. Two things about those builds are worth knowing before installing, and the release notes say both: the APKs are signed with the Flutter template's debug key, and the macOS build is unsigned.

flutter pub get
make run DEVICE=linux   # flutter run -d linux, stamped with version and commit
make linux              # a release build; also apk, apk-tv, apk-split, macos, ios (compile check)

The Makefile adds two --dart-defines so the Diagnostics screen can say which build it is, and its APK targets also set the ABI and a version code that Android installs over an APK from make apk-split. Plain flutter run -d <device> works too and reports app: unknown. A build needs Flutter stable (CI uses 3.47.1) and a Rust toolchain no older than rust-version in rust/Cargo.toml (1.97.1): the Rust crate is compiled by the build itself, through cargokit. Linux desktop also needs clang, cmake, ninja, pkg-config, GTK 3 dev libraries, and libmpv-dev (media_kit links libmpv); Android has a document of its own, ANDROID.md. Everything else a dev machine wants is in docs/OPERATIONS.md.

How it works

┌──────────────────────────────────────────────────────────────┐
│  Flutter UI (this repo) — screens, navigation, playback UI   │
├──────────────────────────────────────────────────────────────┤
│  Dart ⇄ Rust FFI (flutter_rust_bridge), crate in rust/       │
│   • stremio-core   → addons, catalogs, search, library,      │
│                      account, playback state (the "brain")   │
│   • stream-server  → embedded: settings, stats, storage and  │
│                      downloads as FFI calls                  │
├──────────────────────────────────────────────────────────────┤
│  media_kit / libmpv — fetches the media over loopback HTTP,  │
│  decodes and renders it (direct play; codecs and subtitles   │
│  on-device)                                                  │
└──────────────────────────────────────────────────────────────┘

The UI stays thin: discovery/library/addon logic lives in stremio-core, the bytes come from stream-server, and the client's job is presentation plus driving libmpv. stream-server runs in-process: the Rust crate in rust/ links it as a library and starts it on its own thread with its own runtime, bound to 127.0.0.1 on a port the OS picks and retargeting stremio-core at the address it reads back, so no sidecar binary ships and no fixed port is lost to a desktop Stremio. The Dart side never speaks HTTP to it: libmpv fetches the media routes, the app's own questions -- settings, a torrent's stats, storage, downloads -- are FFI calls into the server's library API, and stremio-core's requests to it carry a per-launch bearer token that only the Rust side holds. The only HTTP it serves beyond loopback is the media listener a cast session turns on and off. Because a capable on-device player handles codecs and subtitles, the server never transcodes -- it just gets bytes onto an HTTP connection. Settings can point stremio-core at a remote streaming server by URL, and torrents then play straight off that one; the embedded server still starts and still fronts an addon's direct links. The embedded one is the default.

How that bridge is built, what crosses it and what every field of the state means is in docs/ARCHITECTURE.md.

Every git dependency in rust/Cargo.toml is pinned to a rev with its reason beside it: the three forks are listed in docs/ARCHITECTURE.md.

Platform support

The hard constraint is BitTorrent: the streaming path needs raw TCP/UDP sockets, a local HTTP server, disk cache, and libmpv. That decides everything.

PlatformSupportNotes
Linux (desktop)✅ First-classThe easiest target; video is software-rendered until media_kit's Linux renderer lands (docs/OPERATIONS.md).
Windows (desktop)✅ Built weekly in CI (build.yml)Flutter desktop, media_kit and native Rust, as on Linux; registering stremio:// needs an installer and there is none (docs/DEEP_LINKS.md).
macOS (desktop)✅ Built weekly in CI (build.yml)Native Rust + media_kit; unsigned, and needs a Mac to build yourself -- there is none in the project.
Android✅ SupportedRust cross-compiles to the NDK and is embedded as a native lib; the primary mobile target (ANDROID.md).
Android TV / Google TV✅ SupportedThe same app, not a separate build; install the APK for the ABI the box reports -- a Chromecast with Google TV is 32-bit, make apk-tv (ANDROID.md).
iOS❌ Does not build todayCI compiles it and it fails in an upstream crate (librqbit-dualstack-sockets 0.7.0 calls a socket2 method iOS does not have). Past that, there is no signing identity here, the App Store is out on GPL-3 (see License), and iOS throttles background work.
Web❌ Not possibleA browser cannot do BitTorrent -- no raw sockets, no local server, no libmpv. A thin client onto a separate server is a different architecture, not this app.

What is next

What is genuinely not built:

  • Cloud storage sources (e.g. Google Drive) -- most naturally via a Stremio addon that resolves cloud files to playable URLs; the provider's OAuth or API-key setup is the fiddly part.
  • Media3 remuxing for casting, to let a receiver play a stream it cannot decode as it stands. It would run on the sending device with its platform hardware codec (Android MediaCodec first) -- never ffmpeg, never software transcoding in the Rust core. Until then such a stream is refused rather than mangled.

What is written down where

DocumentWhat is in it
docs/STATUS.mdWhat is built today, screen by screen.
docs/ARCHITECTURE.mdHow the Rust core is wired in: the bridge, what crosses it as JSON, every model field, the pinned forks, and what the app reads from the settings. Its design notes are docs/phase3-design.md.
docs/OPERATIONS.mdWhat to run before a commit, how to see video play, what the server's storage costs, and getting a log off a device.
ANDROID.mdBuilding, running and verifying on Android and Android TV: prerequisites, the APK, the manifest decisions, the emulators, a real box.
docs/CASTING.mdThe cast button: what it hands a receiver untouched, and every rule it refuses on.
docs/ADDONS.mdHow each installed addon has been answering, and the verdict the Installed tab reads off that record.
docs/DEEP_LINKS.mdWhat a stremio:// link may and may not do, and how the scheme is registered on each platform.
AGENTS.mdWhat a change has to satisfy here; see Contributing.

Contributing

AGENTS.md is what a change has to satisfy here: single-concept commits, the verification that gates them, and the rules a real television taught us. Read it before opening a pull request. CI runs the same checks on every push to main and every pull request: formatting, analysis, the Flutter and Rust suites, cargo clippy -D warnings, a cargo check of the core for 32-bit Android, and a check that the flutter_rust_bridge bindings regenerate to what is committed (AGENTS.md has the commands).

License

The source here is MIT (LICENSE). A compiled Xtremio embeds stream-server, which links unrar-rs (GPL-3.0-or-later) so a RAR archive in a torrent plays: distributed binaries are GPL-3.0-or-later (LICENSE-GPL-3.0). unrar-rs asks that a binary reproduce its licence file, the unRAR restriction included, so that text ships in the app too (LICENSE-unrar-rs; Settings → About → Open source licences). stream-server with default-features = false drops both.

Contributors

zond

928 commits

Languages

Dart

78.5%

Rust

18.3%

Kotlin

1.3%