Skip to content

About

Automated 24/7 stream rotation system — downloads YouTube playlists, rotates video content through OBS via VLC, manages stream titles/categories across platforms, and handles live detection for seamless unattended broadcasting.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

OpenStreamRotator

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 ☕

How It Works

  1. 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).
  2. 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.
  3. 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.
  4. Rotation trigger — When all videos have been played and deleted (folder is empty), the system triggers the next rotation automatically.
  5. 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.
  6. 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.
  7. 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.
  8. 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).
  9. 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.
  10. 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.

Prerequisites

  • 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)

Installation

git clone https://github.com/theimperious1/OpenStreamRotator.git
cd OpenStreamRotator

Quick Setup (recommended)

Run the interactive setup script — it walks you through every option and creates your .env file:

Windows:

setup.bat

Linux/Mac:

chmod +x setup.sh
./setup.sh

Then start OSR:

Windows:

run.bat

Linux/Mac:

./run.sh

Manual Setup

If you prefer to configure everything yourself:

pip install -r requirements.txt
cp .env.example .env
# Edit .env with your credentials
python main.py

Configuration

Environment Variables (.env)

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 (config/playlists.json)

{
  "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.

Settings (config/settings.json)

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)

Hot-Swappable Configuration

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:

  1. Open config/settings.json in any text editor.
  2. Change "yt_dlp_use_cookies" from false to true:
    {
      "yt_dlp_use_cookies": true,
      "yt_dlp_browser_for_cookies": "firefox"
    }
  3. 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_cookies and 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:

  1. Set "ignore_streamer": true in config/settings.json and save.
  2. 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.

OBS Setup

On first startup, the system automatically creates any missing scenes and sources in OBS:

  • OSR Stream — with a VLC Video Source named Playlist pointing to your video folder
  • OSR Pause screen — with an Image source pointing to content/pause/default.png
  • OSR Rotation screen — with an Image source pointing to content/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.

Scenes

  1. OSR Stream (default name, configurable via SCENE_STREAM) — The main playback scene. Contains your VLC media source and any overlays. This is what viewers see during normal 24/7 operation.
  2. OSR Pause screen (default name, configurable via SCENE_PAUSE) — Shown when the target streamer goes live. Typically a static image telling viewers the main stream is live.
  3. OSR Rotation screen (default name, configurable via SCENE_ROTATION_SCREEN) — Brief transition scene shown while content folders are being swapped. Can be a loading animation or static image.

VLC Media Source

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_FOLDER path
  • 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.

Kick Setup

When Kick integration is enabled, the program uses OAuth for authentication:

  1. On first startup, a browser window opens automatically to the Kick authorization page.
  2. Log in and authorize the application.
  3. You'll be redirected to a URL containing an authorization code (the page won't load — that's expected).
  4. Copy the full redirect URL from your browser's address bar and paste it into the terminal.
  5. Tokens are saved in core/kick_tokens.db and refreshed automatically for subsequent runs.

Twitch Setup

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.

  1. Create a Twitch application at dev.twitch.tv/console.
  2. Set Client Type to Confidential.
  3. Add your TWITCH_REDIRECT_URI (default: http://localhost:8080/callback) as an OAuth Redirect URL.
  4. Copy the Client ID and Client Secret to your .env file.
  5. On first startup, a browser window opens to the Twitch authorization page.
  6. Log in and click Authorize.
  7. You'll be redirected to a URL like http://localhost:8080/callback?code=abc123... (the page won't load — that's expected).
  8. Copy the full redirect URL from your browser's address bar and paste it into the terminal.
  9. Tokens are saved in core/twitch_tokens.db and 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.

Running

Use the run script (recommended) or start manually:

Windows:

run.bat

Linux/Mac:

./run.sh

Or manually:

python main.py

The system will:

  1. Connect to OBS via WebSocket
  2. Create any missing scenes and sources in OBS (or verify existing ones)
  3. Sync playlist configuration to the database
  4. Resume any interrupted session or start a new rotation
  5. Begin the main automation loop

What Happens Each Rotation

  1. Playlists are selected using weighted priority scoring (see Priority), excluding currently playing and currently downloading playlists
  2. Videos are downloaded via yt-dlp into the pending folder
  3. When downloads complete, OBS switches to the Rotation screen scene
  4. The live folder is cleared and pending content is moved in
  5. Videos are renamed with ordering prefixes (01_video.mp4, 02_video.mp4, etc.) so VLC plays them grouped by playlist
  6. VLC reloads, OBS switches back to the playback scene
  7. Stream title and category are updated on all enabled platforms
  8. The playback monitor begins tracking playback; as each video finishes, it's deleted
  9. When all content is consumed, the cycle repeats

Playlist Priority

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.

Temp Playback

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

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.

Fallback Mode

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:

  1. First 3 retries: Re-attempt the same download (yt-dlp resumes partial files).
  2. 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.

Crash Recovery

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

OBS Freeze Recovery

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:

  1. Polls OBS render frame count every 20 seconds via WebSocket
  2. If frames don't advance for 3 consecutive checks (~60 seconds), OBS is declared frozen
  3. The current stream state (streaming status, playback position) is captured
  4. OBS is killed via taskkill (Windows) or pkill (Linux)
  5. OBS crash sentinel files are cleared to prevent the Safe Mode dialog on relaunch
  6. OBS is relaunched with --minimize-to-tray and --disable-missing-files-check
  7. WebSocket reconnection is attempted automatically
  8. 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.

Skipping Videos

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.

Graceful Shutdown

Press Ctrl+C to stop. The system handles the interrupt cleanly and shuts down.

Discord Notifications

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_transitions in settings.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

Live Detection

The system monitors configured platforms to detect when the target streamer goes live or offline:

  • Twitch — set TARGET_TWITCH_STREAMER to a Twitch username (requires TWITCH_CLIENT_ID / TWITCH_CLIENT_SECRET). Uses Twitch EventSub WebSocket for near-instant detection (stream.online / stream.offline events). HTTP polling runs as an automatic safety-net fallback every 90 seconds.
  • Kick — set TARGET_KICK_STREAMER to a Kick channel slug (requires KICK_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.

EventSub WebSocket (Twitch)

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 broadcasting
  • stream.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_STREAMER must be set
  • TWITCH_CLIENT_ID and TWITCH_CLIENT_SECRET must 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)

Raid-Based Unpause (Twitch only)

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=raid applies to Twitch only. Kick always uses HTTP polling.

Reset State

Use the provided reset script to wipe all state and start fresh:

Windows:

reset_state.bat

Linux/Mac:

chmod +x reset_state.sh
./reset_state.sh

This 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).

Project Structure

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

Web Dashboard

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.

License

See LICENSE for details.

About

Automated 24/7 stream rotation system — downloads YouTube playlists, rotates video content through OBS via VLC, manages stream titles/categories across platforms, and handles live detection for seamless unattended broadcasting.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages