Cast web video to your TV without screen mirroring. Castor extracts streams, converts incompatible formats, and optionally generates subtitles. Run locally or offload media processing to a NAS.
Castor bundles no content or sources and refuses DRM. Use only content you are authorized to access; see Purpose and disclaimer.
Run castor cast to browse titles and cast, without leaving the terminal.
Install on macOS (other platforms) and find your TV:
brew install --cask stupside/tap/castor
castor scanSave config.yaml in your working directory:
device:
name: "Living Room TV" # exact name from `castor scan`
type: dlna # or: chromecast, rokuCast a page:
castor cast player https://example.com/watch/some-video
castor scanfound nothing? See Troubleshooting.
| Command | What it does |
|---|---|
castor scan |
List the devices on your network |
castor cast player <url> |
Cast a web page with an embedded video player |
castor cast url <url> |
Cast a direct stream or video URL |
castor cast |
Browse titles and cast, interactively (needs a TMDB key) |
castor cast movie <id> |
Resolve a movie id against your sources and cast |
castor cast episode <id> --season N --episode N |
Same, for a TV episode |
castor media-server |
Probe, rank, convert, and serve streams |
castor scraping-server |
Extract stream candidates using Chrome |
castor api-server |
Discover/control devices and orchestrate casts |
castor cast --dry-run ... prints the streams it found instead of casting. Run castor --help for all flags.
Use a native binary on your TV's network, with these tools on PATH:
| Tool | Version | Used for |
|---|---|---|
| Chrome / Chromium | Any recent | Finding the video on a page |
| ffmpeg | 7.1+ | Converting the video for your TV |
| ffprobe | 7.1+ | Reading the video's format |
Encoding uses a working GPU encoder, falling back to software.
- macOS:
brew install --cask stupside/tap/castor. - Linux: download
castor_<version>_linux_amd64.tar.gz(or_arm64) from the latest release and putcastoron yourPATH. - Windows: download the release ZIP for your architecture and put
castor.exeonPATH. Install tools withwinget install Gyan.FFmpegandwinget install Google.Chrome. For SmartScreen, choose More info → Run anyway; allow firewall access on private networks for discovery. - From source: see CONTRIBUTING.md.
Castor reads config.yaml (or --config <path>), then overlays git-ignored config.local.yaml. Environment variables such as CASTOR_CAST__MAX_HEIGHT=720 override both. CLI casting needs device; other keys have defaults. Keep secrets out of git; see SECURITY.md.
Subtitles are burned into served, read-once video (e.g. DLNA), not self-fetching Chromecast/Roku playback. The appropriate default model downloads once to your cache.
cast:
subtitles: en # a language code, or auto to detect it; unset for none
whisper:
# model_path: "" # ggml-tiny.en for en, multilingual ggml-tiny otherwiseSet whisper.model_path to use a larger model (e.g. multilingual ggml-base.bin). English-only models reject other languages and auto.
Choose the tallest stream within this ceiling and scale known taller video down:
cast:
max_height: 2160 # default: 1080Title commands substitute IDs into your source templates, trying proxies in order. Add only sites you are authorized to use:
sources:
- proxies: ["https://your-source.example"] # base URLs, tried in order
templates:
movie: "/embed/movie/{itemID}"
episode: "/embed/tv/{itemID}/{season}-{episode}"Only the interactive browser needs a TMDB key:
tmdb:
api_key: "<KEY>"Run on the server with server.token set:
castor media-server # listens on :8410 (server.listen)On your computer:
server:
url: http://my-nas:8410
token: "<a long random string, the same on both>"Your computer discovers and controls the TV; the server processes media. Closing the terminal leaves playback running; Ctrl+C stops it. Cast preferences come from the client, the Whisper model from media. Set server.advertise to an address the TV can reach; use HTTPS outside a trusted network.
Run each command in a separate process; API must reach the TV's network:
castor media-server # :8410
castor scraping-server # :8412
castor api-server # :8411Set server.url: http://localhost:8410 and scraping.url: http://localhost:8412 for API. Configure independent server.token, scraping.token, and api.token; $TOKEN below is the API token.
curl -X POST http://localhost:8411/castor.v1.DeviceService/ListDevices \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{}'
curl -X POST http://localhost:8411/castor.v1.CastService/Cast \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"target": {"deviceId": "<id from ListDevices>"}, "source": {"stream": {"url": "https://example.com/video.m3u8"}}}'For a page, use "source": {"pages": {"urls": ["https://example.com/watch"]}}. Stop ends a cast; Watch streams feedback (use buf curl or a generated client). CLI clients connect with api.url and api.token.
Page casts go API → scraping → media; direct streams bypass scraping. Media never receives webpages. Without api.url, CLI starts missing services on loopback. See ARCHITECTURE.md for contracts, ownership, and lifecycle.
| Protocol | Works with | Status |
|---|---|---|
DLNA / UPnP (MediaRenderer:1) |
Most smart TVs, and players like Kodi, VLC, and Plex | Tested on Samsung |
| Chromecast | Google Cast devices | Experimental, not yet tried on real hardware |
| Roku | Roku TVs and players, through a sideloaded channel | Experimental, not yet tried on real hardware |
Castor sideloads a playback channel:
- Turn on Developer Mode (once, by hand): on the remote press Home x3, Up x2, Right, Left, Right, Left, Right, enable developer mode, and set a web-server password. The device reboots.
- Put the password in your config (out of git, see Configuration):
devices: roku: password: "<dev-web-server-password>" # first cast only
- Cast. Castor sideloads its channel automatically; later casts reuse it.
For a published channel, set devices.roku.app_id instead; no developer mode/password needed. Relayed Roku playback runs about 30 s behind.
Discovery cannot cross VLANs/subnets and is blocked on Android/Termux. Set host to skip discovery:
device:
name: "Living Room TV" # now just a label
type: dlna
host: 192.168.0.3 # the device's LAN IPFor DLNA, host can be a full description URL (e.g. http://192.168.0.3:9197/dmr). On Android/Termux, leave network.interface empty.
The page's video must start without a click. DRM streams are refused.
Force served playback rather than handing the TV a link:
cast:
delivery: serve # "auto" (the default) decides per sourceTry once with CASTOR_CAST__DELIVERY=serve; it costs bandwidth and CPU. castor --debug explains delivery decisions.
The image includes Chrome, ffmpeg, and ffprobe. --device /dev/dri enables Intel VA-API; otherwise encoding uses software.
Warning
The Compose stack targets Linux host networking for LAN discovery and device-reachable media URLs. On macOS/Windows, use the native binary for discovery. Docker Desktop 4.34+ has opt-in host networking, but cannot bind directly to the host's network interfaces.
docker run --rm --network host ghcr.io/stupside/castor:latest scan
docker run --rm --network host --device /dev/dri \
-v "$PWD/config.yaml:/config.yaml" \
-v castor-cache:/root/.cache \
ghcr.io/stupside/castor:latest \
cast player https://example.com/watch/some-videoThe cache volume preserves Whisper models. On Linux, put CASTOR_SERVER__TOKEN, CASTOR_SCRAPING__TOKEN, and CASTOR_API__TOKEN in git-ignored .env, then run docker compose up -d. API and media use host networking; scraping is published only on loopback. Remove the /dev/dri mapping on hosts without Intel VA-API. A standalone bridged media container needs -p 8410:8410 and server.advertise set to a device-reachable address.
Tags: :latest (stable), :canary (preview), or a pinned :vX.Y.Z.
No bundled video or sources; no DRM decryption or circumvention. You are responsible for lawful use and compliance with site terms. Provided as-is for lawful, personal, and educational use.
See CONTRIBUTING.md, and ARCHITECTURE.md for how castor works inside.