The Problem

Casting a web video from a laptop to a TV usually requires screen‑mirroring, which loses resolution and adds latency. Smart‑TV protocols (Chromecast, DLNA) only accept native streams, so users cannot directly cast most online videos.

What This Does

castor automates the whole pipeline:

  • A headless Chrome instance (started from internal/source/extract/*.js) navigates the target page, applies stealth scripts, and captures the underlying video URL via the DevTools protocol.
  • The URL is handed to the FFmpeg pipeline (internal/cast/ffmpeg/*.go) which optionally transcodes the stream, burns subtitles, and feeds it to a replay server (internal/cast/replay/server.go).
  • The replay server advertises the stream over SSDP; the chosen device (internal/device/*.go) receives it and plays it in full quality.

Key entry points are the CLI commands in cmd/ – e.g. cmd/cmd-cast.go implements castInteractive, extractAndCast, and handleStreams. The binary’s main function (main.go:15) dispatches to these commands.

How It Is Wired

Execution begins in main.gomain. The command‑line parser selects one of the cmd‑cast‑*.go files; for interactive casting it calls castInteractive (cmd/cmd-cast.go:38).

castInteractive builds a Cast object (internal/cast/cast.go) and invokes extractAndCast. extractAndCasthandleStreamsListStreams (in internal/source/extract/collector.go) → probeStream (internal/source/resolve/probe.go). probeStream spawns ffprobe via exec.Command, the only external process call.

The most widely referenced functions are:

  • New (16 call sites) – creates core structs (e.g. internal/cast/cue.NewBuilder).
  • Close (15 call sites) – finalizes resources such as the FFmpeg pipe.
  • Run (10 call sites) – drives the TUI model (internal/browse/model.go).

File‑level responsibilities (ordered by call‑graph weight):

FileCore duties
internal/browse/model.go (39 functions) – UI model, fetches TMDB metadata, reads/writes the local cache DB.
internal/browse/tmdb/client.go (28 functions) – TMDB API wrapper, builds poster URLs, parses titles/years.
internal/cast/cue/cue.go (12 functions) – builds cue‑point timeline for subtitle syncing.
internal/cast/ffmpeg/process.go (10 functions) – launches FFmpeg, captures StderrTail, writes to spool.
internal/cast/spool/spool.go (8 functions) – temporary file storage for the transcoded stream.
internal/cast/whisper/transcriber.go (14 functions) – optional speech‑to‑text via local whisper.cpp.
internal/source/extract/collector.go (14 functions) – aggregates network requests captured from Chrome.
internal/device/device.go & internal/device/dlna.go (shared 6‑line blocks) – device discovery and playback control.

External interactions are limited to:

  • FilesystemdownloadFile copies remote assets, StderrTail writes logs, spool writes temporary files.
  • Network – TMDB API calls, SSDP discovery, and the final HTTP stream to the TV.
  • Subprocessffprobe (probeStream) and ffmpeg (process.Start).

No circular import edges were found; the import graph is flat (17 internal modules, 0 cycles). The internal call graph contains 444 edges, with New, Close, and Run having the widest blast radius.

How To Use It

# Clone the repo (exact URL required by the brief)
git clone --recurse-submodules https://github.com/moses-y/castor
cd castor

# Build the binary (requires Go 1.26+, cmake, and Chrome on PATH)
make          # builds libwhisper.a then the castor executable

# Create a minimal config (copy the example)
cp config.yaml.example config.yaml   # config.yaml lives at repository root

# Cast a title by IMDB/TMDB id (interactive UI)
./castor cast movie tt12300742

The Dockerfile can produce a container that bundles Chrome, FFmpeg, and the binary, but it must be run with --network host on Linux for device discovery to work.

Real‑World Use

A home‑automation script can invoke castor after a new episode appears in a download folder:

#!/usr/bin/env bash
NEW=$(ls -t ~/Downloads/*.mkv | head -n1)
./castor cast url "file://$NEW"

The script passes a local file URL; castor treats it as a stream, transcodes if needed, and pushes it to the living‑room TV without leaving the terminal.

Code Health & Issues

Measured findings (static analysis)

  • Medium – Empty catch block in internal/source/extract/js/stealth_canvas.js. Log or rethrow the error.
  • Medium – Duplicated code blocks across cmd/cmd-cast-episode.go, cmd/cmd-cast-movie.go, internal/device/device.go, internal/device/dlna.go. Refactor into shared helpers.
  • Medium – High branching density in internal/cast/gate.go (24 branches / 81 lines). Split into smaller functions or a strategy table.

Code‑health audit

  • Medium – No automated dependency updater – add .github/dependabot.yml.
  • Medium – Docker base image uses mutable tag (cgr.dev/chainguard/wolfi-base:latest). Pin by digest.
  • Medium – No vulnerability‑scan step in CI – integrate dependency-review-action or osv-scanner.
  • Medium – Container runs as root – create a non‑root USER.
  • Low – Workflow jobs lack timeout-minutes – set explicit timeouts.

Other observations: tests exist (*_test.go), CI runs via GitHub Actions, a LICENSE file is present, and no secrets are committed. The repository includes a Makefile, Dockerfile, and go.mod for reproducible builds.

The Bottom Line

castor delivers a functional end‑to‑end solution for extracting and casting web video streams, with a clear Go‑centric architecture and a modest test suite. The codebase is generally healthy but needs routine dependency hygiene, container hardening, and a small refactor to reduce duplicated logic and complex branching. It is suitable for engineers comfortable with Go, Chrome DevTools, and FFmpeg who need a programmable casting tool.