- playout: run-scoped R2 manifest URIs (candidate a rewrite via rewriteManifestToRun) - tests: playlist_rewrite_test.go (rewrite + publish integration + slate guard) - docs: STREAMING, cloudflare-workers-setup, architecture (incl. Level-2 diagram) - release housekeeping: TODO->DONE, CHANGELOG 1.1.0, version bump
Linear Void
Linear Void is a self-managed 24/7 linear video channel for a primarily US audience. It runs a continuous playlist of self-owned videos with true/near-true synchronization, a custom lightweight viewer SPA (hls.js player + chat), and full independence from third-party platforms (YouTube, Twitch, etc.).
Repository: git.allucanget.biz/zwitschi/linearvoid
Architecture
The architecture (detailed in docs/architecture/README.md) uses:
- Hetzner Ashburn VPS for playout (FFmpeg + MediaMTX/SRS) and chat (planned production; currently test lab at 192.168.88.40).
- Cloudflare R2 + CDN with aggressive immutable caching of
.m4ssegments ($0 egress). - Lightweight static frontend and WebSocket chat backend.
- Gitea self-hosted CI/CD (Actions) and Container Registry.
See docs/architecture/adr/ for the full set of decision records (ADR-001 through ADR-007) covering architecture, storage calculations, cache rules, cost model, module structure, tech stack, admin interfaces, data storage, and caching strategy.
Key Features
- Continuous synchronized playback from a playlist (standard HLS, 15–35s latency).
- Self-hosted custom SPA with embedded chat (public rooms + private 1:1 DMs, moderation).
- Extreme cost efficiency via Cloudflare R2 edge caching.
- Video normalization, monitoring, fallback slate, and easy playlist management.
- US-centric performance (Ashburn origin + Cloudflare POPs).
Architecture Diagram
flowchart TD
V[US Viewers] -->|HTTPS| CF[Cloudflare CDN]
CF -->|Cache HIT| R2[Cloudflare R2]
CF -->|Cache MISS| N[Nginx :80]
N -->|/ws| CH[Chat :3000]
N -->|/admin| CH
N -->|/hls| P[Playout :8081]
N -->|/*| FE[Frontend SPA]
CH --> R[(Redis)]
CH --> DB[(PostgreSQL)]
P -->|ffmpeg| HLS[/data/hls/]
P -->|sync| R2
A detailed description of the Streaming architecture, playout, and delivery is available in docs/STREAMING.md.
Project Structure
This structure directly reflects the building blocks, runtime flows, deployment view, and cross-cutting concepts defined in the arc42 documentation.
linearvoid/
├── docs/ # see Documentation section below
├── backend/ # see Backend section below
├── frontend/ # see Frontend section below
├── infrastructure/ # see Infrastructure section below
├── monitoring/ # see Monitoring section below
├── scripts/ # see Scripts section below
├── .editorconfig # EditorConfig for consistent coding styles
├── .env.example # Example environment variables for local development
├── .gitignore # Ignore node_modules, build artifacts, etc.
├── .prettierignore # Ignore files for Prettier formatting
├── CHANGELOG.md # Release notes and version history
├── CONTRIBUTING.md # Contribution guidelines
├── docker-compose.prod.yml # Production deployment (Hetzner + Cloudflare)
├── docker-compose.yml # Top-level for dev (playout + chat + nginx)
└── README.md # This file
Documentation
docs/: Folder containing all documentation for the project, including architecture, ADRs, operational procedures, and setup guides.docs/architecture/: Architecture documentation, including high-level system architecture, component interactions, and decision records (ADRs).
see README.md for full architecture, ADRs, and operational documentation.
Backend
backend/playout/: Core linear engine. FFmpeg consumes a normalized playlist and outputs continuous HLS. Includes normalization validation.backend/chat/: Independent WebSocket service (Node.js/Socket.io or Go). Handles public rooms, private DMs, presence (Redis), persistence (PostgreSQL/SQLite), rate limiting, and moderation.backend/shared/: Reusable code (types, utils, video validation).
backend/
├── chat/ # WebSocket chat service (Node.js or Go)
│ ├── src/ # Chat service source code (rooms, DMs, moderation)
| | ├── auth/ # Admin authentication and session management
| | | ├── auth_test.go # Unit tests for authentication
| | | └── auth.go # Authentication logic (login, session validation)
| | ├── config/ # Configuration files for chat service
| | | ├── config_test.go # Unit tests for configuration
| | | └── config.go # Configuration loading and validation
| | ├── db/ # Database access and models
| | | ├── db_test.go # Unit tests for database access
| | | ├── db.go # Database connection and query logic
| | | ├── models.go # Database models (users, messages, rooms)
| | | └── redis.go # Redis connection and presence management
| | ├── handlers/ # WebSocket message handlers
| | | ├── handlers_test.go # Unit tests for WebSocket message handlers
| | | └── handlers.go # WebSocket message handling logic
| | ├── media/ # Media-related utilities (e.g., video validation)
| | | ├── media_test.go # Unit tests for media-related utilities
| | | └── media.go # Media-related utility functions
| | ├── metrics/ # Prometheus metrics endpoints
| | | ├── metrics_test.go # Unit tests for Prometheus metrics
| | | └── metrics.go # Prometheus metrics collection and exposition
| | ├── ratelimit/ # Rate limiting logic (Redis-based)
| | | ├── ratelimit_test.go # Unit tests for rate limiting
| | | └── ratelimit.go # Rate limiting logic using Redis
| | ├── ws/ # WebSocket handlers
| | | ├── ws_test.go # Unit tests for WebSocket handlers
| | | └── ws.go # WebSocket connection and message handling
| | └── main.go # Entry point for chat service
| ├── Dockerfile # Dockerfile for chat service
| ├── go.mod # Go module file for chat service
| └── go.sum # Go checksum file for chat service
└── playout/ # Continuous HLS generation from playlist (Go)
├── demodata/ # Sample media and playlist files for testing
| ├── media/ # Sample media files for testing
| └── playlist/ # Sample playlist files for testing
├── src/ # Playout service source code (FFmpeg orchestration)
| ├── go.mod # Go module file for playout service
| ├── go.sum # Go checksum file for playout service
| ├── playlist_test.go # Unit tests for playlist management
| └── playlist.go # Playlist management logic
└── testdata/ # Test data for playout service (sample playlists, media)
| ├── comments_only.txt # Sample playlist with comments only
| ├── empty_playlist.txt # Sample empty playlist
| ├── invalid_24fps.mp4 # Sample invalid 24fps video for testing
| ├── invalid_h265.mp4 # Sample invalid H.265 video for testing
| ├── mixed_valid_invalid.txt # Sample playlist with mixed valid and invalid entries
| ├── valid_720p30.mp4 # Sample valid 720p30 video for testing
| └── valid_playlist.txt # Sample valid playlist for testing
├── Dockerfile # Dockerfile for the playout service
└── start-playout.sh # Script to start the playout service
Frontend
The viewer experience — minimal, modern SPA with custom CSS + vanilla JS. Uses hls.js for playback and a WebSocket client for chat.
frontend/
├── admin/ # Admin SPA (playlist management, moderation, analytics)
│ ├── login/ # Login page for admin authentication
| | └── index.html # Admin login page
| └── index.html # Admin SPA
├── public/ # Static assets (favicon, images, etc.)
│ ├── admin/ # Admin static assets
| | ├── analytics.html # Analytics dashboard page
| | ├── chat-log.html # Chat log viewer page
| | ├── dashboard.html # Admin dashboard page
| | ├── moderation.html # User moderation page
| | ├── playlist.html # Playlist management page
| | ├── schedule.html # Schedule overrides page
| | ├── system.html # System configuration page
| | ├── upload.html # Video upload page
| | └── users.html # User management page
│ ├── favicon.ico # Favicon for the SPA
| └── icons.svg # SVG icons for the SPA
├── src/ # Source code for the SPA
│ ├── assets/ # Images, fonts, and other static assets
| | ├── hero.png # Hero image for the SPA
| | ├── typescript.svg # TypeScript logo for the SPA
| | └── vite.svg # Vite logo for the SPA
│ ├── admin.test.ts # Admin frontend tests (Vitest)
│ ├── admin.ts # Admin SPA logic (playlist, moderation, analytics)
│ ├── counter.ts # Counter component for testing
│ ├── main.test.ts # Main frontend tests (Vitest)
│ ├── main.ts # Main SPA logic (hls.js player, WebSocket chat)
│ └── style.css # Custom CSS for the SPA
├── .gitignore # Ignore node_modules, build artifacts, etc.
├── index.html # Main SPA entry point
├── package-lock.json # Lockfile for npm dependencies
├── package.json # Build tools (Vite recommended)
├── tsconfig.json # TypeScript configuration
├── vite.config.ts # Vite configuration
└── vitest.config.ts # Vitest configuration
Infrastructure
Everything needed to deploy and configure the stack (Docker, Nginx, Cloudflare rules, future Terraform).
- Cloudflare
- linearvoid-media worker — R2-bound edge Worker serving HLS on
media.linearvoid.tv(immutable segment caching, CORS, Range) - linearvoid-frontend worker — serves the static SPA on
linearvoid.tv
- linearvoid-media worker — R2-bound edge Worker serving HLS on
- Nginx
- Dockerfile — builds Nginx with custom config
- nginx.conf — reverse proxy, static SPA, HLS delivery
Monitoring
Grafana dashboards, Prometheus rules, logging configuration.
- Grafana
- dashboards
- dashboard.json — operational dashboards for playout, chat, and Redis
- dashboard.yml — operational dashboards for playout, chat, and Redis
- datasources
- datasource.yml — Prometheus datasource configuration
- dashboard.json — operational dashboards for playout, chat, and Redis
- dashboards
- Prometheus
- alerts.yml — alerting rules for playout, chat, Redis, and system health
- prometheus.yml — scrape targets for playout, chat, Redis, and system metrics
Scripts
One-off and CI-friendly scripts for video preparation, R2 sync, backups, and playlist management.
-
loadtest/
- admin-api.js — admin API load test script
- run-loadtests.sh — bash script to run ws and/or admin load tests
- summarize-artillery.sh — summarize Artillery JSON results
- summarize-k6.sh — summarize k6 JSON results
- ws-chat.yml — WebSocket chat load test configuration
PowerShell equivalents (
*.ps1) of the three load-test scripts are kept alongside them.
Getting Started
Note: The
docker compose upbelow targets local development. The test lab (192.168.88.40) receives automated CD deploys. Production Hetzner VPS is not yet provisioned — seeDEPLOYMENT.md.
git clone https://git.allucanget.biz/zwitschi/linearvoid.git
cd linearvoid
cp .env.example .env
# Edit .env with your R2 credentials and domain settings
docker compose up --build
Then open:
- Viewer: https://linearvoid.tv (or
http://localhost) - Admin: https://linearvoid.tv/admin (or
http://localhost/admin) - Metrics: https://linearvoid.tv/metrics
Full setup guide: SETUP.md.
See the architecture docs for detailed architecture and ADRs.
Development
- Use Docker Compose for local development (playout + chat + frontend).
- Test lab host:
192.168.88.40(internal network, receives CD deploys). Production Hetzner VPS TBD. - Public production host model:
https://linearvoid.tvfor viewer/admin/chat andhttps://media.linearvoid.tvfor HLS delivery. - Normalize all source videos before adding to playlist (see ADR-001 for exact codec/fps/keyframe requirements).
- Monitor cache hit ratio and R2 operations closely — they determine real cost.
Environment Variables
| Variable | Default | Description |
|---|---|---|
R2_ACCESS_KEY_ID |
— | Cloudflare R2 access key |
R2_SECRET_ACCESS_KEY |
— | Cloudflare R2 secret key |
R2_BUCKET_NAME |
linearvoid-media |
R2 bucket for HLS segments |
DATABASE_URL |
postgres://...@db:5432/... |
Chat DB (Postgres or SQLite) |
REDIS_URL |
redis://redis:6379/0 |
Redis for presence/rate limiting |
VITE_PUBLIC_APP_URL |
https://linearvoid.tv |
Public viewer URL |
VITE_PUBLIC_WS_URL |
wss://linearvoid.tv/ws |
WebSocket endpoint |
VITE_PUBLIC_HLS_URL |
https://media.linearvoid.tv/hls/playlist.m3u8 |
HLS playlist URL |
ADMIN_USERNAME |
admin |
Admin panel username |
ADMIN_PASSWORD |
— | Admin panel password |
HLS_SEGMENT_DURATION |
2 |
HLS segment duration in seconds |
HLS_WINDOW_SIZE |
12 |
HLS playlist window size |
Full reference: .env.example.
Troubleshooting
| Problem | Check |
|---|---|
| Playout not starting | Verify playlist exists in data/playlist/playlist.txt and media files are accessible |
| HLS not playing | Check docker compose logs playout for ffmpeg errors; verify data/hls/ permissions |
| Chat not connecting | Ensure Redis is running: docker compose logs redis; check DATABASE_URL |
| Admin panel 401 | Verify ADMIN_USERNAME/ADMIN_PASSWORD in .env match login credentials |
| Docker build fails | Run docker compose build --no-cache; check backend/playout/.dockerignore |
| R2 upload failures | Verify R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY; check bucket CORS settings |
API Endpoints
| Service | Endpoint | Description |
|---|---|---|
| Chat | GET /health |
Health check |
| Chat | GET /metrics |
Prometheus metrics |
| Chat | GET /ws |
WebSocket upgrade for chat |
| Chat | GET /api/admin/* |
Admin API (auth required) |
| Chat | POST /api/admin/login |
Admin login |
| Playout | GET /health |
Health check (port 8081) |
| Nginx | GET / |
Viewer SPA |
| Nginx | GET /admin/ |
Admin panel |
| Nginx | GET /hls/playlist.m3u8 |
HLS manifest |
Architecture Documentation
The architecture is fully documented using the arc42 template (with populated sections, Mermaid context and building-block diagrams, deployment view, cross-cutting concepts, quality goals, risks, and glossary).
Decision Records (in docs/architecture/adr/):
- ADR-001: Overall system architecture, HLS + Cloudflare R2 caching strategy, storage calculations, cost model, risks, and implementation plan.
- ADR-002: Chosen project folder/module structure (frontend, backend/playout, backend/chat, infrastructure, scripts, monitoring) and mapping to arc42 building blocks.
- ADR-003: Technology stack, language choices per module (TypeScript for frontend, Go preferred for chat and playout wrapper, Bash/Python for scripts), Docker-first approach, and all implicit assumptions.
- ADR-004: Video admin web interface (upload, playlist management, schedule overrides, normalization enforcement).
- ADR-005: Web admin interfaces for user management, moderation dashboard, analytics, and system configuration.
- ADR-006: Data storage strategy (SQLite vs PostgreSQL for v1, migration path, admin interface capabilities).
- ADR-007: Redis usage (exact use cases, TTL policies, persistence, fallback behavior, admin configuration).
See the architecture README for the big-picture view and the individual ADR files for detailed rationale, alternatives, and consequences.
Contributions, issues, and further evolution (LL-HLS, ABR, multi-channel, E2EE) should reference the architecture documentation.
Resources
- CHANGELOG.md - Version history and completed features.
- CONTRIBUTING.md - Guidelines for contributing to the project.
- SETUP.md - Complete setup guide with prerequisites, environment configuration, running the application, and troubleshooting.
- API.md - Implemented WebSocket and REST API reference.
- DEPLOYMENT.md - Production deployment and operations guide.
- MONITORING.md - Prometheus and Grafana setup and troubleshooting.
- SECURITY.md - Security controls, hardening checklist, and incident baseline.
- Scaling Playbook - Performance test method, thresholds, and scaling decision flow.
- Architecture Documentation - Full arc42 template with diagrams and decisions.
- ADRs - Detailed decision records (ADR-001 to ADR-007).
CI/CD: View workflows on Gitea Actions.
Container Registry: git.allucanget.biz/zwitschi/linearvoid/<service>:latest
Contributing
Contributions are welcome! See CONTRIBUTING.md for guidelines. Quick overview:
- Read the architecture docs and ADRs first.
- Code against the defined module structure (frontend, backend/playout, backend/chat, infrastructure, scripts, monitoring).
- Open PRs on Gitea.
- CI validates builds and tests automatically.
Since this is a linear project, we also rely on a linear development and release model: only main branch is used for releases, and all features are merged into main after review and CI validation. Releases use semantic version tags in *.*.* format (for example 1.0.9) and are automatically built/pushed to the container registry. Version numbers are declared in frontend/package.json, CHANGELOG.md, and the version badge in this README.
The project is ready for use. See SETUP.md for detailed installation instructions.