Fully automated 24/7 stream rerun system. Downloads YouTube playlists, plays them through VLC in OBS, rotates content when finished, and manages stream metadata on Kick and Twitch — all unattended.
Like this project? Buy me a coffee ☕
- Playlist selection — Each rotation, the system picks a configurable number of enabled playlists and downloads their videos via yt-dlp into a pending folder. Selection is weighted by play history and priority — higher-priority playlists are selected more frequently (see Priority below).
- Content switch — Once downloads finish, OBS briefly shows a transition scene while the live folder is swapped with the new content. VLC reloads and playback begins.
- Playback monitoring — The system detects video transitions via OBS WebSocket media events (
MediaInputPlaybackStarted/MediaInputPlaybackEnded). When a video finishes, it's deleted and the next one is tracked automatically. - Rotation trigger — When all videos have been played and deleted (folder is empty), the system triggers the next rotation automatically.
- Stream metadata — The stream title and category are updated on all enabled platforms (Kick, Twitch) each rotation. Categories update per-video based on which playlist the video came from.
- Temp playback — If the current content runs out before the next rotation's downloads finish, the system temporarily plays already-downloaded files from the pending folder to avoid dead air.
- Prepared rotations — Pre-download future rotations to a staging folder for on-demand or scheduled execution via the web dashboard. Supports custom titles, categories, and optional cursor restore.
- Fallback mode — If yt-dlp downloads fail repeatedly (3 consecutive failures by default), the system arms fallback mode. Once all live content and temp playback are exhausted, it activates: cycling through fallback-marked prepared rotations (with full title/category support), or the pause screen if none are available. Retries downloads every 5 minutes with escalation (retry pending, then try fresh playlists).
- Twitch EventSub live detection — Real-time stream lifecycle events via Twitch EventSub WebSocket (
stream.online/stream.offline) provide near-instant pause and unpause — no polling delay. HTTP polling remains as an automatic fallback. Optionally supports raid-based unpause (UNPAUSE_MODE=raid) — if the streamer raids out but forgets to end their stream, the 24/7 stream resumes immediately. - OBS freeze detection — The system monitors OBS render output via WebSocket. If OBS stops rendering frames for ~60 seconds (process alive but frozen), it automatically kills OBS, clears crash sentinels, relaunches it, reconnects, and resumes streaming — all unattended.
- Python 3.10+
- OBS Studio 28+ with the OBS WebSocket plugin (v5, built into OBS 28+)
- VLC Media Player (used as a media source inside OBS)
- yt-dlp (installed automatically via pip, but must be on PATH for cookie extraction)
git clone https://github.com/theimperious1/OpenStreamRotator.git
cd OpenStreamRotatorRun the interactive setup script — it walks you through every option and creates your .env file:
Windows:
setup.batLinux/Mac:
chmod +x setup.sh
./setup.shThen start OSR:
Windows:
run.batLinux/Mac:
./run.shIf you prefer to configure everything yourself:
pip install -r requirements.txt
cp .env.example .env
# Edit .env with your credentials
python main.py| Variable | Required | Default | Description |
|---|---|---|---|
ENABLE_TWITCH |
No | false |
Enable Twitch integration (optional — not required if only using Kick) |
TWITCH_CLIENT_ID |
If Twitch enabled | — | Twitch application client ID |
TWITCH_CLIENT_SECRET |
If Twitch enabled | — | Twitch application client secret |
TWITCH_USER_LOGIN |
If Twitch enabled | — | Your 24/7 Twitch channel username |
TWITCH_REDIRECT_URI |
No | http://localhost:8080/callback |
OAuth redirect URI for Twitch |
TARGET_TWITCH_STREAMER |
No | (empty) | Streamer whose live status pauses the rerun. If empty, live detection is disabled — ideal for pure 24/7 streams. |
UNPAUSE_MODE |
No | offline |
offline = unpause on stream.offline EventSub event (or offline poll as fallback). raid = also unpause immediately when the streamer raids out via channel.raid (whichever fires first). Twitch only — Kick has no raid feature. |
ENABLE_KICK |
No | false |
Enable Kick integration (optional — not required if only using Twitch) |
KICK_CLIENT_ID |
If Kick enabled | — | Kick application client ID |
KICK_CLIENT_SECRET |
If Kick enabled | — | Kick application client secret |
KICK_CHANNEL_ID |
No | (auto-resolved) | Your 24/7 Kick channel ID. Leave empty — auto-resolved during first OAuth login. |
KICK_REDIRECT_URI |
If Kick enabled | http://localhost:8080/callback |
OAuth redirect URI for Kick |
TARGET_KICK_STREAMER |
No | (empty) | Kick channel slug whose live status pauses the rerun. If empty, Kick live detection is disabled. |
OBS_HOST |
No | localhost |
OBS WebSocket host |
OBS_PORT |
No | 4455 |
OBS WebSocket port |
OBS_PASSWORD |
Yes | — | OBS WebSocket password |
SCENE_PAUSE |
No | OSR Pause screen |
OBS scene shown when target streamer is live (pauses 24/7 content) |
SCENE_STREAM |
No | OSR Stream |
OBS scene for normal 24/7 playback |
SCENE_ROTATION_SCREEN |
No | OSR Rotation screen |
OBS scene shown during rotation transitions |
VLC_SOURCE_NAME |
No | OSR Playlist |
Name of the VLC media source in OBS |
DISCORD_WEBHOOK_URL |
No | — | Discord webhook for notifications |
WEB_DASHBOARD_URL |
No | — | URL of the OpenStreamRotator Web Dashboard backend (e.g. https://your-domain:8000) |
WEB_DASHBOARD_API_KEY |
No | — | API key from the web dashboard Team page (generated when you register an instance) |
VIDEO_FOLDER |
No | content/live/ |
Path to the live playback folder (VLC reads from here) |
NEXT_ROTATION_FOLDER |
No | content/pending/ |
Path to the pending/download folder |
BROADCASTER_ID |
No | — | Twitch broadcaster ID (auto-resolved from TWITCH_USER_LOGIN if empty) |
OBS_PATH |
No | (auto-detected) | Full path to obs64.exe. Auto-detected from standard install locations if not set. |
{
"playlists": [
{
"name": "MY PLAYLIST",
"url": "https://www.youtube.com/playlist?list=PLxxxxxx",
"category": "Just Chatting",
"enabled": true,
"priority": 1
}
]
}| Field | Description |
|---|---|
name |
Display name used in stream titles and logs |
url |
YouTube playlist URL |
category |
Platform category set when videos from this playlist are playing (e.g. "Just Chatting", "Final Fantasy XIV"). Falls back to the playlist name if not set. |
enabled |
Whether this playlist is available for rotation selection |
priority |
Selection frequency multiplier (default 1). A priority of 2 means the playlist is selected ~2x as often, 3 = ~3x as often, etc. See Priority below. |
All settings in this file are hot-swappable — you can edit and save while the program is running and changes take effect within seconds. See Hot-Swappable Configuration below for details and examples.
{
"min_playlists_per_rotation": 2,
"max_playlists_per_rotation": 2,
"download_retry_attempts": 5,
"stream_title_template": "24/7 Reruns | {PLAYLISTS} | ",
"yt_dlp_verbose": false,
"notify_video_transitions": false,
"ignore_streamer": false,
"yt_dlp_use_cookies": false,
"yt_dlp_browser_for_cookies": "firefox"
}| Field | Description |
|---|---|
min_playlists_per_rotation |
Minimum playlists selected per rotation |
max_playlists_per_rotation |
Maximum playlists selected per rotation |
download_retry_attempts |
Number of retry attempts for failed downloads |
stream_title_template |
Title template. {PLAYLISTS} is replaced with playlist names joined by | (legacy {GAMES} also accepted) |
yt_dlp_verbose |
Enable verbose yt-dlp output in logs |
notify_video_transitions |
Send a Discord notification on every video transition (default: false, can be noisy with short videos) |
ignore_streamer |
Prevents the target streamer going live from pausing the 24/7 stream (default: false) |
yt_dlp_use_cookies |
Use browser cookies for age-restricted videos (default: false). Toggle mid-download-retry to recover from 403s. |
yt_dlp_browser_for_cookies |
Browser to extract cookies from: chrome, firefox, brave, edge, etc. (default: firefox) |
Both config/settings.json and config/playlists.json can be changed while the program is running — no restart required. The system re-reads both files every loop iteration (once per second), so your changes take effect almost immediately.
.env values are also hot-swappable when using the web dashboard. The dashboard writes changes to .env and triggers a live reload — credentials, target streamers, OBS connection settings, scene names, and more are all updated without restarting. If editing .env by hand (without the dashboard), a restart is required.
| File | Hot-swappable | What lives here |
|---|---|---|
config/settings.json |
Yes | All runtime behavior settings |
config/playlists.json |
Yes | Playlist definitions (enable/disable, priorities, URLs) |
.env |
Yes (via dashboard) / No (manual edit) | Credentials, target streamers, OBS config, scene names |
Example — Recovering from YouTube 403 errors mid-download:
If downloads start failing with 403 (Forbidden) errors — common when YouTube detects automated requests — you can enable cookie-based authentication on the fly without stopping the program:
- Open
config/settings.jsonin any text editor. - Change
"yt_dlp_use_cookies"fromfalsetotrue:{ "yt_dlp_use_cookies": true, "yt_dlp_browser_for_cookies": "firefox" } - Save the file. The program re-reads it before each download attempt, so the very next retry will use cookies from your browser session (where you're logged into YouTube).
This works because the download retry loop reads cookie settings fresh on every attempt. If the program is in the middle of retrying a failed download (with exponential backoff between attempts), your change will be picked up on the next retry — no content interruption.
Tip: Make sure you're logged into YouTube in the browser specified by
yt_dlp_browser_for_cookiesand that the browser is closed (some browsers lock their cookie database while running).
Example — Ignoring streamer live status:
If the target streamer goes live and you want to keep your 24/7 stream running instead of pausing:
- Set
"ignore_streamer": trueinconfig/settings.jsonand save. - The program ignores live-status checks until you set it back to
false.
Example — Adjusting rotation size:
Want more variety in the next rotation? Change max_playlists_per_rotation from 2 to 4 in config/settings.json and save. The next rotation will pick up to 4 playlists.
Example — Disabling a playlist mid-run:
Want to stop a playlist from being selected in future rotations? Open config/playlists.json, set "enabled": false on that playlist, and save. It won't be picked for the next rotation.
On first startup, the system automatically creates any missing scenes and sources in OBS:
OSR Stream— with a VLC Video Source namedPlaylistpointing to your video folderOSR Pause screen— with an Image source pointing tocontent/pause/default.pngOSR Rotation screen— with an Image source pointing tocontent/rotation/default.png
All sources are automatically sized to fill the canvas. You can replace the default images with your own, add overlays, or customize the scenes in OBS as needed.
OSR Stream(default name, configurable viaSCENE_STREAM) — The main playback scene. Contains your VLC media source and any overlays. This is what viewers see during normal 24/7 operation.OSR Pause screen(default name, configurable viaSCENE_PAUSE) — Shown when the target streamer goes live. Typically a static image telling viewers the main stream is live.OSR Rotation screen(default name, configurable viaSCENE_ROTATION_SCREEN) — Brief transition scene shown while content folders are being swapped. Can be a loading animation or static image.
The VLC source is created automatically inside the OSR Stream scene. If you need to configure it manually, add a VLC Video Source (not a regular Media Source) named OSR Playlist (configurable via VLC_SOURCE_NAME) to your stream scene with these settings:
- Playlist directory: Point it to your
VIDEO_FOLDERpath - Loop: Enabled (the system handles advancement by deleting played files; loop ensures VLC keeps playing)
- Shuffle: Disabled (the system controls ordering via filename prefixes)
The VLC source plugin for OBS must be installed separately if not already present.
When Kick integration is enabled, the program uses OAuth for authentication:
- On first startup, a browser window opens automatically to the Kick authorization page.
- Log in and authorize the application.
- You'll be redirected to a URL containing an authorization code (the page won't load — that's expected).
- Copy the full redirect URL from your browser's address bar and paste it into the terminal.
- Tokens are saved in
core/kick_tokens.dband refreshed automatically for subsequent runs.
Twitch uses the OAuth Authorization Code flow for channel updates (title and category). Live status checking uses a separate app token that's generated automatically.
- Create a Twitch application at dev.twitch.tv/console.
- Set Client Type to Confidential.
- Add your
TWITCH_REDIRECT_URI(default:http://localhost:8080/callback) as an OAuth Redirect URL. - Copy the Client ID and Client Secret to your
.envfile. - On first startup, a browser window opens to the Twitch authorization page.
- Log in and click Authorize.
- You'll be redirected to a URL like
http://localhost:8080/callback?code=abc123...(the page won't load — that's expected). - Copy the full redirect URL from your browser's address bar and paste it into the terminal.
- Tokens are saved in
core/twitch_tokens.dband auto-refresh when they expire. You only need to do this once.
If you change your Twitch password or disconnect the app, the refresh token becomes invalid and you'll be prompted to re-authorize on next startup.
Use the run script (recommended) or start manually:
Windows:
run.batLinux/Mac:
./run.shOr manually:
python main.pyThe system will:
- Connect to OBS via WebSocket
- Create any missing scenes and sources in OBS (or verify existing ones)
- Sync playlist configuration to the database
- Resume any interrupted session or start a new rotation
- Begin the main automation loop
- Playlists are selected using weighted priority scoring (see Priority), excluding currently playing and currently downloading playlists
- Videos are downloaded via yt-dlp into the pending folder
- When downloads complete, OBS switches to the Rotation screen scene
- The live folder is cleared and pending content is moved in
- Videos are renamed with ordering prefixes (
01_video.mp4,02_video.mp4, etc.) so VLC plays them grouped by playlist - VLC reloads, OBS switches back to the playback scene
- Stream title and category are updated on all enabled platforms
- The playback monitor begins tracking playback; as each video finishes, it's deleted
- When all content is consumed, the cycle repeats
The priority field in playlists.json controls how often a playlist is selected relative to others. It works as a frequency multiplier:
| Priority | Effect |
|---|---|
1 |
Normal — selected in its regular turn (default) |
2 |
Selected ~2x as often as a priority-1 playlist |
3 |
Selected ~3x as often as a priority-1 playlist |
5 |
Selected ~5x as often as a priority-1 playlist |
How it works: The system scores each playlist by multiplying its time-since-last-played by its priority. A priority-3 playlist that was played 2 hours ago gets an effective age of 6 hours, making it compete with playlists that haven't been played for much longer. Playlists that have never been played always come first regardless of priority.
Example: With 20 playlists and 4 per rotation, a priority-1 playlist appears roughly every 5th rotation. A priority-3 playlist would appear roughly every other rotation instead.
If the current rotation's content runs out before the next rotation's downloads finish, the system enters temp playback mode:
- Already-downloaded files in the pending folder are played immediately
- Videos are deleted after playing (an archive file prevents yt-dlp from re-downloading them)
- When all downloads complete, a normal content switch happens
This prevents dead air during long downloads.
Prepared rotations let you pre-download specific playlists into a staging folder and execute them on demand or on a schedule via the web dashboard. While a prepared rotation is playing:
- The live folder is temporarily swapped for the prepared content
- Stream title and category are updated based on the prepared playlists
- When the prepared rotation content finishes (or is manually stopped), the system restores the original live content and resumes where it left off (if "Resume where I left off" was enabled)
Prepared rotations are managed entirely through the web dashboard — create, schedule, execute, and delete.
If yt-dlp downloads fail repeatedly (3 consecutive failures by default), the system arms fallback mode. Fallback does not activate immediately — it waits until all live content is naturally consumed and temp playback can no longer fill the gap. This ensures viewers aren't interrupted while content is still playing.
Once live content is exhausted, fallback activates with the best available option:
| Tier | Name | Condition | Behavior |
|---|---|---|---|
| 1 | Prepared rotation | One or more prepared rotations marked as "fallback" exist and are ready | Cycles through all fallback rotations in order, looping back to the first when the last one finishes. Full title/category support per rotation. |
| 2 | Pause screen | No fallback content available | Switches to the pause scene |
While in fallback, the system retries downloads every 5 minutes with an escalating strategy:
- First 3 retries: Re-attempt the same download (yt-dlp resumes partial files).
- Subsequent retries: Wipe the pending folder and try a completely fresh rotation with different playlists — in case the original playlists are broken or blocked.
When a download succeeds, fallback mode is automatically deactivated and normal operation resumes.
You can mark any ready prepared rotation as a fallback candidate via the web dashboard's "Use as fallback" toggle. Multiple fallback rotations can be designated — the system cycles through all of them in creation order. The dashboard also provides a "Deactivate" button to manually exit fallback mode.
The system tracks session state in a local SQLite database. If the process is restarted:
- If videos remain in the live folder, playback resumes from the last saved position (cursor is saved every second)
- The stream title and category are restored automatically
- If a temp playback session was active, it's restored
- If mid-download, downloads resume (yt-dlp handles partial file resumption)
- A Discord notification is sent with the resumed session ID and playback position
Separate from crash recovery, the system detects frozen OBS processes. OBS may appear running but stop rendering frames (GPU hang, driver issue, etc.). The freeze monitor:
- Polls OBS render frame count every 20 seconds via WebSocket
- If frames don't advance for 3 consecutive checks (~60 seconds), OBS is declared frozen
- The current stream state (streaming status, playback position) is captured
- OBS is killed via
taskkill(Windows) orpkill(Linux) - OBS crash sentinel files are cleared to prevent the Safe Mode dialog on relaunch
- OBS is relaunched with
--minimize-to-trayand--disable-missing-files-check - WebSocket reconnection is attempted automatically
- Streaming is resumed if it was active before the freeze
Freeze recovery is retried on future freezes as long as the previous recovery succeeded. If a recovery fails, further automatic restarts are blocked to avoid restart loops — a notification is sent and manual intervention is required.
You can skip videos directly in OBS by advancing the VLC source. The playback monitor will detect the transition naturally — the skipped video gets deleted and the next video is tracked.
Press Ctrl+C to stop. The system handles the interrupt cleanly and shuts down.
If DISCORD_WEBHOOK_URL is set, the system sends notifications for:
- Automation Started / Shutting Down — program lifecycle events
- Now Playing — playlist names after a content switch completes
- Session Resumed — crash recovery with video name and timestamp
- Temp Playback Activated / Complete — long download handling
- Video Transition — per-video notifications (opt-in via
notify_video_transitionsinsettings.json) - Rotation downloads — started, ready, errors, warnings
- Fallback mode — activated (with tier) and deactivated notifications
- Stream metadata failures — title/category update errors
- Streamer live/offline — target streamer status changes
- Automation errors — unexpected exceptions
The system monitors configured platforms to detect when the target streamer goes live or offline:
- Twitch — set
TARGET_TWITCH_STREAMERto a Twitch username (requiresTWITCH_CLIENT_ID/TWITCH_CLIENT_SECRET). Uses Twitch EventSub WebSocket for near-instant detection (stream.online/stream.offlineevents). HTTP polling runs as an automatic safety-net fallback every 90 seconds. - Kick — set
TARGET_KICK_STREAMERto a Kick channel slug (requiresKICK_CLIENT_ID/KICK_CLIENT_SECRET). Uses HTTP polling every 30 seconds. Kick has no EventSub equivalent.
If both are configured, either platform being live triggers the pause. If neither is set, live detection is skipped entirely — ideal for pure 24/7 streams (e.g., music or ambient content) that should never pause.
- Streamer goes live → OBS switches to the pause scene, rotations are postponed
- Streamer goes offline → OBS switches back to the playback scene, normal operation resumes
This is designed for 24/7 rerun channels that should yield to the main streamer.
When TARGET_TWITCH_STREAMER is set with valid Twitch credentials, the system automatically connects a lightweight EventSub WebSocket to Twitch on startup. This provides:
stream.online— instant pause when the streamer starts broadcastingstream.offline— instant unpause when the stream ends
No additional configuration is needed — it activates automatically. If the WebSocket connection drops, the system falls back to HTTP polling transparently and reconnects with exponential backoff.
Requirements:
TARGET_TWITCH_STREAMERmust be setTWITCH_CLIENT_IDandTWITCH_CLIENT_SECRETmust be configured- A Twitch user token must be stored (created automatically during the first Twitch platform auth flow via
TWITCH_BROADCASTER_ID/TWITCH_USER_LOGIN)
Setting UNPAUSE_MODE=raid adds a channel.raid subscription to the same EventSub WebSocket. This handles the edge case where a streamer raids out but forgets to end their stream — the stream.offline event may be delayed by up to ~20 minutes.
When a raid is detected, the 24/7 stream resumes immediately. All three triggers remain active (stream.offline, channel.raid, and the HTTP poll fallback); whichever fires first wins.
Note: Kick has no raid feature, so
UNPAUSE_MODE=raidapplies to Twitch only. Kick always uses HTTP polling.
Use the provided reset script to wipe all state and start fresh:
Windows:
reset_state.batLinux/Mac:
chmod +x reset_state.sh
./reset_state.shThis deletes the SQLite database and all downloaded videos from both the live and pending folders (paths are read from VIDEO_FOLDER / NEXT_ROTATION_FOLDER env vars).
OpenStreamRotator/
├── main.py # Entry point
├── config/
│ ├── config_manager.py # Loads playlists.json and settings.json
│ ├── constants.py # Application constants
│ ├── playlists.json # Playlist definitions
│ └── settings.json # Runtime settings (hot-swappable)
├── controllers/
│ ├── automation_controller.py # Main orchestration loop
│ └── obs_controller.py # OBS WebSocket interface
├── core/
│ ├── database.py # SQLite session, playlist, and playback tracking
│ └── video_registration_queue.py # Async queue for video file registration
├── handlers/
│ ├── content_switch_handler.py # OBS scene transitions during rotation
│ ├── dashboard_handler.py # Web dashboard command handler
│ └── temp_playback_handler.py # Temp playback during long downloads
├── integrations/
│ └── platforms/
│ ├── base/stream_platform.py # Platform interface
│ ├── kick.py # Kick API integration
│ └── twitch.py # Twitch API integration
├── monitors/
│ └── obs_freeze_monitor.py # OBS freeze detection and recovery
├── managers/
│ ├── download_manager.py # Download orchestration and retry logic
│ ├── obs_connection_manager.py # OBS WebSocket connection lifecycle
│ ├── platform_manager.py # Multi-platform broadcast orchestration
│ ├── playlist_manager.py # Folder management and video renaming
│ ├── prepared_rotation_manager.py # Prepared rotation scheduling and execution
│ ├── rotation_manager.py # Rotation lifecycle management
│ └── stream_manager.py # Stream title and category updates
├── playback/
│ └── playback_monitor.py # Event-driven video transition detection via OBS WebSocket
├── services/
│ ├── kick_live_checker.py # Kick live status polling
│ ├── notification_service.py # Discord webhook notifications
│ ├── twitch_live_checker.py # Twitch live status polling
│ └── web_dashboard_client.py # WebSocket client for the web dashboard
├── utils/
│ ├── playlist_selector.py # Rotation playlist selection logic
│ ├── video_downloader.py # yt-dlp download wrapper
│ ├── video_processor.py # Video file processing utilities
│ └── video_utils.py # Video duration and validation helpers
└── content/
├── live/ # Current playback folder (VLC reads from here)
├── pause/ # Pause screen assets
├── pending/ # Next rotation downloads
├── prepared/ # Prepared rotation content staging
└── rotation/ # Rotation screen assets
OpenStreamRotator has an optional web dashboard for remote monitoring and control. See OpenStreamRotatorWeb for setup instructions.
To connect an OSR instance to the dashboard, set WEB_DASHBOARD_URL and WEB_DASHBOARD_API_KEY in your .env file. The API key is generated when you register an instance on the Team page of the web dashboard.
See LICENSE for details.
