Stream the media stored on your Freebox over FTP, from anywhere,
without copying anything to the server.
Unofficial community project, free and open source.
Not developed, endorsed or supported by Free / Iliad. Do not ask them for help with it.
The files stay on the Freebox. A small Go service exposes them to ffmpeg as a
seekable HTTP resource, transcodes them on demand to H.264, and serves the
result as HLS to a browser. It is built for one person watching their own
library while travelling, on hotel wifi or 4G.
Put whatever you like in your library: 4K HEVC 10-bit remuxes are welcome and nothing is ever refused for being too big. What is capped is the encoder output, at 720p by default and 1080p at most — a 2160p source is simply downscaled on the way out. The two output options are switchable per playback from the detail page.
The library mirrors your own folder tree, and every thumbnail is a frame taken from the middle of the file — not artwork fetched from a catalogue.
- No local copy, ever.
ffmpegreads HTTP Range requests from a loopback proxy that translates them into FTPREST/RETR. Seeking into the middle of a 60 GB remux downloads only the part being watched. - Seeking without seek logic. The HLS playlist is generated from the probed duration before anything is encoded, so the player treats the file as an ordinary VOD asset. A segment request is answered from disk, from the running encoder, or by starting a new encoder at exactly that point.
- Downscales, never upscales. Any source resolution is accepted; the encoder
just never emits more than 1080p, so a 2160p remux comes out at 1080p or 720p.
In the other direction it leaves the source alone: a 480p file stays 480p at
both settings, and shares a single cache entry instead of being encoded twice.
The output height is always
min(chosen rendition, source height). - Audible dialogue. Multichannel audio is downmixed to stereo, which is the difference between a watchable 5.1 film on a phone and an inaudible one.
- Survives restarts. The segment cache lives on disk and is reconciled with its index at startup; playback resumes where it stopped.
- No online catalogue. Titles come from your own folder and file names, and posters are a frame taken from the middle of each video. If you want a synopsis, you write it. Nothing is sent anywhere.
ffmpegandffprobe(5.x or later)- A Freebox with FTP enabled, reachable from the server
- Go 1.26+ to build from source
- A machine that can encode H.264 (see below)
Every stream is transcoded, unconditionally: there is no direct-play path. So the only question that matters when picking a box is whether it can produce H.264 fast enough, one way or another.
The rule of thumb: a Pi 5 is a downgrade from a Pi 3 or a Pi 4 for this workload, which is counter-intuitive enough to be worth stating plainly. The generation that lost the encoder lost the job.
"OK" is not unconditional on either Pi, though:
- The hardware encoder tops out at 1080p30 on both, and its output quality is noticeably worse than x264 at the same bitrate.
- Neither can decode HEVC in realtime, and the Pi 3 has no HEVC decoder at all. Both are fine for an H.264 / 1080p library, not for Blu-ray remuxes.
- The Pi 3 is the tighter of the two: 1 GB of RAM, and its Ethernet shares the USB 2.0 bus, so pulling a high-bitrate source off the Freebox is the bottleneck well before the encoder is.
The default is software libx264 (-preset veryfast -crf 23), which is what
the reference deployment uses: a Ryzen 5800H measures about 16x realtime at
720p, ample for the one or two streams this is designed for.
On a board without that headroom, point VIDEO_ENCODER at the hardware block:
VIDEO_ENCODER |
Where |
|---|---|
libx264 (default) |
anywhere with a capable CPU |
h264_v4l2m2m |
Raspberry Pi 3 and Pi 4 |
h264_vaapi |
Linux with /dev/dri, Intel or AMD |
h264_qsv |
Intel Quick Sync |
h264_nvenc |
NVIDIA |
h264_videotoolbox |
macOS and Apple Silicon |
Rate control is not portable between them, so each carries its own arguments
rather than a substituted codec name: -crf exists only on x264,
-global_quality on QSV, -cq on NVENC, and VAAPI has to upload and scale on
the GPU, which changes the filter chain. An encoder your ffmpeg was not built
with is refused at startup, listing what is available, rather than failing
mid-film.
Changing encoder changes the cache key, so previously encoded segments are kept but no longer used: x264 and NVENC produce different bitstreams and must never be stitched together.
The point of the split: the Freebox is never asked to transcode. It serves byte ranges over FTP, which is what it is good at, and every frame of CPU work happens on the machine running Proxystream. Nothing is copied there either: seeking to 1h20 of a 60 GB remux fetches only that region.
git clone https://github.com/aluzed/freebox-proxystream.git
cd freebox-proxystream
cp .env.example .env
$EDITOR .env # FTP_HOST, FTP_USER, FTP_PASS
docker compose up -dThen open the service and create the admin account on the first-run screen.
compose.yaml publishes on 127.0.0.1:8080 by default. Expose it on your
Tailscale interface instead, or put a reverse proxy in front: the service
speaks plain HTTP and has no TLS, so binding it to 0.0.0.0 puts an
unencrypted login on your whole LAN.
There is deliberately no account in .env: credentials live in the database, so
the configuration file holds no secret. For a headless install where nobody can
reach that screen, create the account from the CLI instead:
docker compose run --rm proxystream useradd alexSee CLI.md for users and passwd as well.
cp .env.example .env && $EDITOR .env
go build -o proxystream ./cmd/proxystream
./proxystreamThen create the account at http://localhost:8080, or from the CLI with
./proxystream useradd <username>.
- Réglages → browse the Freebox FTP tree, pick a directory, name it, and say whether it holds films or series. Creating a library starts a scan.
- The scanner lists the directory, probes each new file through the range proxy (header and index only, not the whole file), and derives a title from the folder and file names.
- Pick a title, choose quality, audio track and subtitles, and press play.
Progress is saved every 10 seconds and offered as a resume point on the detail
page. Keyboard shortcuts in the player: space, ←/→ for 10s, F fullscreen,
M mute.
Text tracks (SRT, ASS, mov_text) are extracted to WebVTT on demand and cached.
Bitmap tracks from Blu-ray rips (PGS, VobSub) cannot be decoded by any browser;
they are listed but disabled rather than silently dropped, since displaying them
would require burning them into the video and a separate encode per track.
Everything is in .env; see .env.example for the annotated
list, and CLI.md for the binary's subcommands and operational
recipes. The ones worth knowing:
| Variable | Default | Meaning |
|---|---|---|
DEFAULT_HEIGHT |
720 |
Output rendition used when the player does not ask for one. 720 or 1080; the server refuses to start on any other value. This caps the encode, not the library. |
VIDEO_MAXRATE_720 / _1080 |
3M / 6M |
Bitrate ceiling per rendition. 720p is sized for hostile networks. |
CACHE_MAX_BYTES |
50 GiB | Ceiling for the segment cache, enforced by LRU eviction. |
CACHE_TTL |
336h |
Segments untouched for this long are dropped. |
MAX_CONCURRENT_ENCODERS |
2 |
Beyond this, the least recently used encoder is reclaimed. |
ENCODER_IDLE_TIMEOUT |
90s |
An encoder nobody is reading from is killed. |
API_TOKEN |
empty | Optional bearer token for scripting. Disabled when empty. |
PREFERRED_LANG |
fr |
Default audio track language, ISO 639-1. |
Credentials are not among them: the username and its bcrypt hash live in the SQLite database, so the config file and the repository stay free of secrets.
The one design decision everything else follows from: ffmpeg does not read
FTP. It reads HTTP from a loopback proxy that answers 206 Partial Content
with an exact Content-Length and Accept-Ranges: bytes. That is what makes
the input seekable, and therefore what makes -ss jump straight to a byte
offset instead of decoding from the beginning. Feed ffmpeg a pipe instead and
the entire seek design collapses.
A few consequences worth keeping in mind when changing things:
- Segments are MPEG-TS, not fMP4. They are produced by independent ffmpeg invocations that get stitched together, and TS segments are self-contained. fMP4 would need one shared init segment whose codec parameters match across every session.
-output_ts_offsetputs each run on the source's absolute timeline, so a run starting at segment 0 and one starting at segment 300 join without a discontinuity.- FTP control connections are never reused after an aborted transfer. A reader that is closed early takes its whole connection down with it. Redialling costs a few hundred milliseconds and avoids an entire category of bug.
- Changing the audio track or the quality changes the variant key, and therefore starts a fresh encode. That is expected.
Work is tracked as one Markdown file per story in docs/stories/,
each with a status in its front matter so anything can be picked up or handed
over without re-deriving the context. It is empty at the moment: everything
opened so far has shipped.
grep -h '^status:' docs/stories/*.md | sort | uniq -cgo test ./... # 65 tests; the ffmpeg ones are skipped with -short
go test ./... -short # unit tests only
go vet ./...The suite runs a real in-process FTP server and real ffmpeg, so it exercises
the range semantics, the encode, and the seek path rather than mocks.
No user accounts beyond the single owner, no quality ladder or ABR, no direct play or remux fast path, no GPU acceleration, no live TV, no PGS burn-in, no Chromecast or DLNA, no offline download, no public internet exposure, no TLS. Remote access is Tailscale's job.
MIT, see LICENSE. Contributions welcome.
This is a community project, not an official one. It is written and maintained by users, it is not developed, reviewed, endorsed or supported by Free / Iliad in any way, and it uses no private or reverse-engineered API: it talks to the Freebox over plain FTP, exactly as any FTP client would.
Freebox is a trademark of Free / Iliad, used here only to say which device the project talks to. If something breaks, open an issue here — Free's support has nothing to do with this software and cannot help with it.



