# Stream – Torrent Streaming on Low-End Android TVs

> Built an Android TV app that plays torrents through its own Rust BitTorrent engine, keeping a moving 256 MiB window of the film on the TV, so a 1 GB stick needs no streaming server, account or backend.

- Author: Daniil Hirchyts
- Published: November 10, 2026
- URL: https://hirchyts-daniil.me/projects/stream
- Link: https://github.com/Daniil-Hirchyts/stream

## Introduction

Stream is an Android TV app for films and series from Stremio add-ons. Its BitTorrent engine is built into the app, so a torrent plays directly on the TV. Nothing else has to run: no server on a computer in the next room, no debrid service, no account and no backend. The library and history stay on the TV.

It is a personal project, and it was built for one kind of hardware: the cheap streaming stick. The reference device is a first-generation Fire TV Stick, with 1 GB of RAM, a 32-bit ARM processor and a few gigabytes of free storage. **A two-hour film doesn't fit on it, so the engine never tries to store one.** It keeps a fixed cache of 128, 256 or 512 MiB, holding the next few seconds of the film and a little of what was just watched, whatever the size of the file.

I wrote the whole app, from the remote-driven interface to the engine. In under two weeks it went from the first commit to a public release with APKs for each processor type. The engine is about 11,000 lines of Rust and replaced an earlier C++ engine built on libtorrent.

![Browsing Home with the remote: the spotlight follows focus across current films and series, then opens a film's details](https://hirchyts-daniil.me/projects/stream/home.mp4)

## The Constraint: A TV That Can't Hold the Film

Most ways of watching a torrent on a TV assume a second machine. Stremio streams torrents through a separate server process, which is closed-source. TorrServe is a server that other players connect to. Plex and Jellyfin play files that a home server already holds. That second machine is the part a household with one cheap stick doesn't have.

Doing it on the stick itself means three limits at once:

- **Storage.** A 1080p film can be 7 GB, and the stick has less than that free. The engine can only keep a small part of the file.
- **Memory.** The whole app targets 250 MiB, which has to cover the interface, artwork, the player's buffers and the engine together.
- **Seeking.** A viewer who jumps 30 minutes ahead expects a picture in seconds, but those bytes may not have been fetched yet.

## How the Engine Streams a Torrent

The engine only downloads what playback needs, around the position being read.

1. **Nothing downloads until Play.** Opening a title may look up its sources so that Play is instant, but no data is fetched until the viewer presses it. A magnet link's metadata comes from peers.
2. **A window moves with the playhead.** From the player's position the engine keeps about 45 seconds of media at download priority, bounded by the cache. The piece the player waits for and the next two get deadlines, checked every half second.
3. **Urgent pieces go to the fastest peers.** A block one peer is slow to send is asked of another after two seconds. Every piece is hash-checked before the player can read it.
4. **Watched pieces are evicted.** When the cache has no free slot, the least recently used verified piece outside the window is dropped, and peers are told so they stop asking for it. The file's first and last pieces stay, because video containers keep their headers and indexes there.
5. **A seek moves the window.** Requests for pieces that left it are cancelled, and the pieces at the new position become urgent at once.

The cache is one file, allocated up front, so it can never grow past its size. Even its size accounts for the filesystem: Android's ext4 counts its own extent blocks against the app, so the engine leaves 64 KiB of the budget for them.

Play the film below, seek, and change the cache size. Then switch to a plain download of the whole file and watch the same TV run out of space.

When a source fails or stalls, the app starts the next one at the same position with the same audio and subtitle tracks. Sources are ranked by resolution, size, seeders and how they worked before, so the first try is usually the right one.

![The sources for Tears of Steel, sorted, with quality and size for each](https://hirchyts-daniil.me/projects/stream/sources.jpg)

## Why Rust Replaced libtorrent

The first engine was C++ around libtorrent, the library behind most desktop clients. It worked, but streaming through a bounded cache needs one thing libtorrent doesn't offer: forgetting a verified piece so its space can be reused. I maintained a patch to libtorrent to get it, so the eviction logic lived half in my code and half in a modified copy of someone else's library.

I first checked whether an existing Rust client could replace it. librqbit refused pure BitTorrent v2 torrents and had no way to forget a piece, since it stores data per file rather than per piece. So I wrote the BitTorrent implementation myself, and on 30 October the Rust engine replaced the C++ one in a single change: libtorrent, Boost, OpenSSL, the patches and the CMake build all went.

What changed for the project:

| | C++ engine on libtorrent | Rust engine |
| --- | --- | --- |
| Eviction | A patch to a third-party library | Part of the engine's own cache and piece picker |
| Build for three Android processor types | CMake, plus fetched Boost and OpenSSL sources | Cargo with a pinned toolchain and the Android NDK |
| A bug in engine code | An abort takes the whole app down | A panic unwinds and is caught at the boundary with the app |
| Data from peers | Parsed in C++ | Parsed with size limits in memory-safe code |
| HTTPS to trackers and web seeds | OpenSSL | rustls, with the trust roots taken from Android |

The eviction row turned out to matter most. A thirty-minute emulator soak test seeks every ten seconds. The C++ engine passed it once and failed it once, timing out at seek 159 after 27 minutes. The failed run kept no record of its state, so its cause could not be proven. The Rust engine runs over a pluggable backend, so a test can drive it with a model swarm and lose one eviction report on purpose. That test showed that both engines then stopped evicting for good, which matches the timeout. Because eviction was now the engine's own code, the fix was too: it abandons a report that hasn't arrived within ten seconds. It has passed the soak twice since, the second time with 186 seeks, a median recovery of 1.7 seconds and no hash failures.

Pick a fault below and watch each engine handle it.

Rust didn't make the protocol better than libtorrent. libtorrent has years of exposure to real swarms that this engine doesn't. The gain was in owning the parts that streaming depends on, and in being able to test them.

## Testing Without Touching a Real Swarm

The tests never use public torrents. The engine is checked on loopback against independent clients in both directions: libtorrent, Transmission, aria2, WebTorrent, librqbit and plain HTTP servers acting as web seeds. Each transfer must deliver the exact file, checked by its SHA-256 hash. Those runs found two real defects, both now fixed with regression tests: a uTP-only session ignored the configured port, and a peer reconnecting from an address it had just used was refused.

On top of that, the integration tests play the hostile peer: corrupt pieces, dropped requests, false metadata sizes and peers that never read their replies. A seek stress test makes 200 seeks through a 16 MiB cache at the emulator's speed and checks every byte.

## The App Around the Engine

The engine is one part of a full TV app:

- **Made for a remote.** Every screen works with the D-pad, OK and Back, and also with touch or a pointer.
- **Decoding.** The TV's hardware decoders are used when they support the format. Bundled FFmpeg decoders take over for DTS, TrueHD, 10-bit video and older codecs.
- **Privacy.** Favourites, history and add-on settings are stored on the TV and encrypted with an Android Keystore key. Playable stream links are never stored, and Android backup is turned off.
- **Small downloads.** The APK for 32-bit sticks is 15 MB.

![The player's subtitle style menu, previewing a font over Tears of Steel](https://hirchyts-daniil.me/projects/stream/player-subtitle-style.jpg)

## Impact and Learnings

Stream plays a torrent of any size on a TV that holds a quarter of a gigabyte of it, with no other machine involved. It is open source under the GPL, with releases on GitHub.

Its limits are written down too. Every test so far ran on the Android TV emulator, not on the physical stick. One slow peer still limits playback to its speed. Stream does not host or index any content: add-ons are third-party services that the viewer chooses.

What I would repeat:

- **Bound the resource before writing the feature.** A cache that is allocated up front and cannot grow removes a whole class of failure on a device with no room to spare.
- **Own the part the product depends on.** Patching a large library for the one behavior that mattered was harder to reason about than implementing it.
- **Keep the evidence of a failure.** The timeout at seek 159 could not be explained because the run kept nothing. The soak test now saves its state, logs and a screenshot when it fails.
