Cast web video to your TV, even when it has no built-in casting like Chromecast. Point it at any web page and it finds the video, transcodes it, and streams it to your TV in real time. It even burns in subtitles.
It runs as one command on your laptop, or as services you can move to a NAS.
Important
Castor ships no content and no sources, and refuses DRM streams. Use it only for video you are allowed to watch.
brew install --cask stupside/tap/castor # macOS
npm install -g @castor-dev/cli # any platform, Node 20+Or download the latest release, or build from source.
Note
Castor needs Chrome and ffmpeg + ffprobe 7.1+ on your PATH. On Windows: winget install Google.Chrome and winget install Gyan.FFmpeg, then allow Castor on private networks.
Settings go in a config.yaml (where).
Give castor a page that plays a video: it finds the stream, converts what your TV can't play, and starts playback.
Casting a page to a DLNA TV with castor cast player
-
Find your TV:
castor scan
-
Add it to
config.yaml, with the name and type the scan printed:device: name: "Living Room TV" type: dlna # dlna, chromecast, or roku
-
Cast a page, or a direct stream or video file:
castor cast player https://example.com/watch/some-video castor cast url https://example.com/video.m3u8
Useful flags: --at 1h12m to start at a position, --dry-run to list the streams found without casting, --debug to see why castor does what it does.
castor cast opens a title browser in your terminal: search TMDB or browse what is trending, pick a title and a TV, and castor finds it on your sources.
Picking a title in castor cast
-
Add a free TMDB key and the sites you are authorized to use:
tmdb: api_key: "<KEY>" sources: - proxies: ["https://your-source.example"] templates: movie: "/embed/movie/{itemID}" episode: "/embed/tv/{itemID}/{season}-{episode}"
-
Run it. The configured device, if any, is preselected:
castor cast
To skip the TUI, castor cast movie <id> and castor cast episode <id> --season N --episode N cast to the configured device.
Castor is one binary made of three services:
- API server finds your TVs and controls playback. Every client talks to it.
- Scraping server opens the page in headless Chrome and captures the video stream.
- Media server transcodes with ffmpeg when your TV needs it, and serves the stream.
flowchart LR
client["castor CLI / TUI<br/>or another app"] -- castor.v1 API --> api["API server"]
api --> scraping["Scraping server<br/>(Chrome)"]
api --> media["Media server<br/>(ffmpeg)"]
api -- controls playback --> tv["TV"]
media -- video --> tv
castor cast starts all three in its own process, so there is nothing to set up. Each one is also a command (castor api-server, castor media-server, castor scraping-server) you can run on another machine. Give castor its URL in config.yaml and it uses that server; anything without a URL still runs locally.
| Setup | Run on the NAS | Add to your config.yaml |
|---|---|---|
| Everything on your computer (default) | nothing | nothing |
| Transcoding on the NAS | castor media-server |
servers.media.url and its token |
| Everything on the NAS | docker compose up -d |
servers.api.url and its token |
The annotated config.yaml describes every setting. Castor reads it from your user config directory or the current directory:
| System | User config directory |
|---|---|
| Linux | ~/.config/castor/ |
| macOS | ~/Library/Application Support/castor/ |
| Windows | %AppData%\castor\ |
Layers merge in this order, each over the one before:
castor/config.yamlin your user config directoryconfig.yamlin the current directory- the file passed with
--config <path> CASTOR_environment variables, such asCASTOR_CAST__MAX_HEIGHT=720
Keep secrets, like your TMDB key, in a git-ignored config.local.yaml next to the file. Coming from 1.x? See the migration guide.
| Protocol | Status | Reported working on |
|---|---|---|
| DLNA / UPnP | Main target | Samsung Q70 and Q80 (#15), Philips 50PUD6654 (#23) |
| Chromecast | Experimental | Xiaomi TV Box (#5), LG C2 in Chromecast mode, from the Android client (#68) |
| Roku | Experimental | Not tested on hardware yet |
Also reported working, without saying which protocol:
- Hisense VIDAA TV, in the device discussion
Cast to another TV? Tell us how it went in the What device do you cast to? discussion.
Setting up a Roku
- Enable Developer Mode: on the remote, press Home three times, Up twice, then Right Left Right Left Right, and set a web server password.
- Put that password in
servers.api.devices.roku.passwordinconfig.local.yaml. - Cast: castor installs its channel the first time.
| Platform | Install with | Notes |
|---|---|---|
| macOS, Intel and Apple silicon | Homebrew, npm, release | |
| Linux, x86-64 and arm64 | npm, release, Docker | The Docker image encodes on Intel GPUs (VA-API) |
| Windows, x86-64 and arm64 | npm, release | Allow castor on private networks |
| Android, in Termux | Not documented yet | Reported by @d4nnABR (#24). Discovery fails there, so set device.host to the TV's IP |
| Android, as an app | castor-android | A community project |
Castor uses a hardware encoder when ffmpeg has one that works, and software otherwise.
The Docker image bundles Chrome and ffmpeg. Run it with Linux host networking, so the NAS can find your TVs and they can reach it.
Your computer still controls the TV; the NAS transcodes.
-
On the NAS, with
servers.media.tokenin itsconfig.yaml(add--device /dev/drifor Intel VA-API):docker run -d --network host -v "$PWD/config.yaml:/config.yaml:ro" \ ghcr.io/stupside/castor:latest media-server -
On your computer:
servers: media: url: http://my-nas:8410 token: "<same token as the NAS>"
The NAS runs all three services; your computer, and any other client, only sends commands.
-
On the NAS, next to docker-compose.yml and a
config.yaml(it can be empty), put one token per service in.env:CASTOR_SERVERS__API__TOKEN=<random> CASTOR_SERVERS__MEDIA__TOKEN=<random> CASTOR_SERVERS__SCRAPING__TOKEN=<random>
-
Run
docker compose up -d. Remove its/dev/drilines if the NAS has no Intel GPU. -
On your computer:
servers: api: url: http://my-nas:8411 token: "<CASTOR_SERVERS__API__TOKEN>"
Ports, HTTPS and image tags are in ARCHITECTURE.md.
Everything the CLI does goes through the public castor.v1 API: list devices, find the streams on a page, start, watch or stop a cast. Use it over Connect, with generated clients or plain HTTP and JSON.
- castor-android by @kdpkke: an Android app that casts from your phone, with castor running on it.
Built something on castor? Open a pull request to add it here.
| Symptom | Check |
|---|---|
| No devices found | Discovery stays on one subnet. Set device.host to the TV's IP (always on Android/Termux). |
| Two devices share a name | Set device.id to the ID castor scan prints. |
| Page video not found | The video must start without a click; DRM streams are refused. |
| TV loads but cannot play | Force relayed playback: CASTOR_CAST__DELIVERY=serve castor --debug cast url <url>. |
| TV cannot fetch the video | Allow Castor through the firewall; check servers.media.advertise. |
Ports castor uses, for your firewall
| Port | Direction | Used for |
|---|---|---|
| UDP 1900 | Discovery | Finding DLNA TVs and Rokus (SSDP) |
| UDP 5353 | Discovery | Finding Chromecasts (mDNS) |
| A random LAN port, or TCP 8410 | TV to castor | The TV fetching the video from the embedded media server, or from a standalone one |
| TCP 8411, 8412 | Clients to castor | A standalone API server and scraping server |
| TCP 8009, 8060 | Castor to TV | Controlling a Chromecast, or a Roku (port 80 to install the channel) |
A DLNA TV is controlled on whatever port it advertises.
Bug reports, device reports and fixes are welcome: CONTRIBUTING.md explains how to build castor and what fits the project. Castor is provided as is, for personal and educational use; you are responsible for using it lawfully and within each site's terms.