CaptureStream is a lightweight, self-hosted live-stream recorder written in Python. It continuously polls a configured list of creators on Twitch, YouTube Live and TikTok Live and, as soon as one of them goes live, records the stream as a local VOD.
- Web UI — add a streamer by pasting a URL or typing a username, just like streamrecorder.io, and watch live status in real time.
- One thread per streamer — detections and recordings run in parallel.
- Polite polling with jitter so you stay well below platform rate limits.
- Automatic reconnect if a stream briefly drops mid-broadcast.
- Clean, structured output:
Recordings/<platform>/<streamer>/<date>_<time>_*.mp4. - Built-in recordings browser with direct downloads.
CaptureStream/
├── main.py # Entry point + loop manager
├── config.json # Example config (streamers + general settings)
├── requirements.txt # Python dependencies
├── recorder/ # Platform logic (checkers + downloaders)
│ ├── __init__.py
│ ├── base.py # BaseRecorder + StreamInfo + state
│ ├── manager.py # Runtime add/remove + config persistence
│ ├── twitch.py # Twitch checker (GQL) + streamlink recorder
│ ├── youtube.py # YouTube Live probe + streamlink recorder
│ ├── tiktok.py # TikTok Live probe + yt-dlp recorder
│ └── utils.py # Logging, filename helpers, URL parser
├── web/ # Web UI
│ ├── server.py # Flask REST API + werkzeug server
│ └── static/ # index.html, app.js, style.css
└── README.md
- Python 3.10+
streamlink(used for Twitch and YouTube capture)yt-dlp(used for TikTok capture — handles both HLS and FLV)ffmpegon your PATH (both tools use it for muxing)
The Python dependencies are listed in requirements.txt.
Clone the repository and create an isolated virtual environment:
git clone https://github.com/<your-user>/CaptureStream.git
cd CaptureStream
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtMake sure ffmpeg is installed and available on your PATH:
- macOS:
brew install ffmpeg - Ubuntu/Debian:
sudo apt install ffmpeg - Windows: install from https://www.gyan.dev/ffmpeg/builds/ and add
bin/to PATH.
streamlink and yt-dlp are installed from requirements.txt and expose CLI
binaries inside the active virtual environment. If you install them system-wide
instead, make sure both are reachable from your shell (streamlink --version,
yt-dlp --version).
Edit config.json (or pass your own file via --config). Example:
{
"general": {
"output_dir": "Recordings",
"check_interval_seconds": 240,
"jitter_seconds": 30,
"stream_quality": "best",
"log_level": "INFO",
"filename_template": "{date}_{time}_{streamer}_{title}.mp4",
"max_filename_length": 180
},
"streamers": [
{ "platform": "twitch", "username": "shroud", "enabled": true },
{ "platform": "youtube", "username": "@LofiGirl", "enabled": true },
{ "platform": "tiktok", "username": "tiktok", "enabled": false }
]
}Notes:
check_interval_secondsis the base poll cadence per streamer (default 4 min). A randomjitter_secondsoffset is added on each cycle to avoid synchronised bursts. The minimum effective interval is 60 s.stream_qualityis passed tostreamlink(e.g.best,720p,audio_only).filename_templatesupports the placeholders{date},{time},{streamer},{title},{platform}. Invalid filesystem characters are replaced automatically.- YouTube
usernameaccepts handles (@name), channel slugs, or channel IDs (UC…, 24 characters). - TikTok
usernameis the@handle (with or without the@prefix).
YAML configs are also supported if you install PyYAML (already in the
requirements) — just pass --config config.yaml.
By default, CaptureStream starts both the polling manager and the web UI
(on http://127.0.0.1:7878):
python main.py
# or with a custom config / host / port:
python main.py --config my-config.json --host 0.0.0.0 --port 8080
# headless (no web UI):
python main.py --no-webYou should see log output like:
2026-04-23 12:34:56 [INFO] manager :: Watching 2 streamer(s); poll interval 240s (±30s).
2026-04-23 12:34:56 [INFO] web :: Web UI available at http://127.0.0.1:7878/
2026-04-23 12:34:57 [INFO] twitch:shroud :: Watching — waiting for streams…
2026-04-23 12:39:01 [INFO] twitch:shroud :: Live detected (VALORANT ranked grind) — starting recording.
2026-04-23 12:39:01 [INFO] twitch:shroud :: Recording to Recordings/twitch/shroud/2026-04-23_12-39-01_shroud_VALORANT ranked grind.mp4
Press Ctrl+C (or send SIGTERM) to stop gracefully — active recordings are
terminated cleanly and any in-flight file is flushed.
Open http://127.0.0.1:7878 in your browser. You can:
- Add a streamer by pasting a channel URL — Twitch, YouTube, or TikTok:
https://twitch.tv/<channel>https://youtube.com/@<handle>orhttps://youtube.com/channel/UC…https://tiktok.com/@<username>
- Add by username: type just the name (e.g.
shroud) and pick the platform from the dropdown. YouTube handles without@are auto-prefixed. - See live status update every few seconds:
waiting,live,recording, orerror, with each streamer's last-check timestamp. - Stop watching a streamer with the Remove button — the change is
persisted back to
config.jsonimmediately. - Download recordings from the Recordings table; files stream directly
from the configured
output_dir.
Changes made through the UI are written to config.json atomically (temp file
- rename) so you always have a consistent snapshot on disk.
The UI is a thin client over this JSON API, which you can use from your own scripts too:
| Method | Path | Description |
|---|---|---|
GET |
/api/streamers |
List watched streamers with status. |
POST |
/api/streamers |
Body: `{ "input": "", "platform": "twitch |
DELETE |
/api/streamers/<id> |
Stop watching. <id> is platform:username. |
GET |
/api/recordings |
List recorded files. |
GET |
/recordings/<path> |
Download a recorded file (sandboxed to output_dir). |
GET |
/api/health |
Liveness probe. |
- The server binds to
127.0.0.1by default. Only bind to0.0.0.0on a trusted network — there is no authentication. - The download endpoint resolves and re-verifies every path against
output_dir, and only serves files with known media suffixes (.mp4,.mkv,.ts,.flv,.webm).
Inside BaseRecorder.record each platform's downloader (streamlink/yt-dlp) is
already configured with its own per-segment retry flags. If the downloader
itself exits with a non-zero status while the stream is still live, CaptureStream
re-checks the live state and relaunches it — up to MAX_RECONNECT_ATTEMPTS (5)
times per session, with a short backoff between attempts. Once the recheck
reports "not live", the recorder returns to the polling state.
This project is intended for personal, archival use of content you are allowed to record (for example, your own streams or content under an appropriate license). Always respect each platform's Terms of Service and the creators' rights when using this tool.