{"id":50356819,"url":"https://github.com/wvogel/eyestream","last_synced_at":"2026-05-29T23:04:16.137Z","repository":{"id":349215517,"uuid":"1201490199","full_name":"wvogel/eyestream","owner":"wvogel","description":"Self-hosted video platform with HLS adaptive streaming, admin UI, and embed player. FastAPI + ffmpeg + Nginx, behind OAuth2-Proxy.","archived":false,"fork":false,"pushed_at":"2026-04-18T15:47:16.000Z","size":2455,"stargazers_count":1,"open_issues_count":13,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-18T17:35:39.692Z","etag":null,"topics":["docker","fastapi","ffmpeg","hls","nginx","oauth2-proxy","oembed","open-source","postgresql","self-hosted","streaming","video","video-cdn"],"latest_commit_sha":null,"homepage":"https://github.com/wvogel/eyestream","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wvogel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-04T18:44:07.000Z","updated_at":"2026-04-18T15:46:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/wvogel/eyestream","commit_stats":null,"previous_names":["wvogel/eyestream"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/wvogel/eyestream","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wvogel%2Feyestream","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wvogel%2Feyestream/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wvogel%2Feyestream/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wvogel%2Feyestream/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wvogel","download_url":"https://codeload.github.com/wvogel/eyestream/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wvogel%2Feyestream/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33673656,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["docker","fastapi","ffmpeg","hls","nginx","oauth2-proxy","oembed","open-source","postgresql","self-hosted","streaming","video","video-cdn"],"created_at":"2026-05-29T23:04:15.284Z","updated_at":"2026-05-29T23:04:16.125Z","avatar_url":"https://github.com/wvogel.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Eyestream\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Tests](https://github.com/wvogel/eyestream/actions/workflows/test.yml/badge.svg)](https://github.com/wvogel/eyestream/actions/workflows/test.yml)\n[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)\n[![Docker](https://img.shields.io/badge/docker-compose-blue?logo=docker\u0026logoColor=white)](https://docs.docker.com/compose/)\n\nSelf-hosted video platform with HLS adaptive streaming, admin UI, and embed player. Upload videos, encode them automatically into multiple quality levels, and deliver them via static Nginx — no app server in the streaming path.\n\n![Eyestream Admin UI](docs/screenshot-overview.png)\n\n## Architecture\n\n![Architecture](docs/architecture-en.svg)\n\n## Requirements\n\n- Docker and Docker Compose (v2+)\n- A reverse proxy with HTTPS (see below)\n- An OIDC provider for authentication (Entra ID, Authentik, Keycloak, Google, etc.)\n\n## Quick Start\n\n```bash\ncp .env.example .env                   # Edit with your settings\ncp oauth2-proxy.env.example oauth2-proxy.env  # Edit with your OIDC provider\ndocker network create shared-npm 2\u003e/dev/null || true\ndocker compose up -d --build\n```\n\nOpen `https://your-admin-domain/` to access the admin UI.\n\n## Reverse Proxy\n\nEyestream requires a reverse proxy that terminates HTTPS and forwards traffic to the containers. Two services need to be reachable from the proxy:\n\n- **Public Nginx** (container `eyestream-public`) — serves HLS streams, embed player, posters\n- **OAuth2-Proxy** (container `eyestream-oauth2-proxy`) — serves the admin UI behind authentication\n\nThe stack joins an external Docker network (`shared-npm` by default) where your reverse proxy runs. Any reverse proxy works:\n\n| Proxy | Log-based referer analysis |\n|-------|---------------------------|\n| [**Nginx Proxy Manager**](https://github.com/NginxProxyManager/nginx-proxy-manager) | Yes — mount NPM's log directory and set `NPM_SITE_ID` |\n| **Traefik**, **Caddy**, **nginx**, etc. | Proxy works, but view counts and referer tracking on the stats page will not be available (these rely on NPM's access log format) |\n\n```bash\n# Create the shared network if it doesn't exist\ndocker network create shared-npm\n```\n\nAdjust the network name in `docker-compose.yml` if you use a different one.\n\n## Features\n\n### Video Management\n- Upload with category assignment and notes\n- Inline editing (title: double-click or pencil, notes: contenteditable with search highlighting)\n- Categories for organization with filter dropdown\n- Autocomplete search across titles, notes, and category names\n- Disable/enable videos (public URLs return 404, admin retains full access)\n- Custom poster from any video frame (ffmpeg extraction from original, not canvas capture)\n- Hover preview (6 rotating thumbnails, 320px admin + 960px embed)\n- Duration badge, view count badge, referer site badges on poster\n- Configurable items per page (10/20/50/100/All, persistent)\n\n### Streaming \u0026 Embedding\n- HLS Adaptive Bitrate (1080p, 720p, 480p, 360p via CMAF/fMP4)\n- Public embed player (`/embed/{id}`) with poster, hover thumbnails, click-to-play\n- oEmbed endpoint for automatic rich embeds (Outline Wiki, etc.)\n- Raw `.m3u8` URL for custom player integration\n\n### Statistics (`/stats`)\n- Video counts with status breakdown\n- Total duration, average encoding factor\n- Disk usage pie chart (uploads, HLS, system, free)\n- Category breakdown (interactive sorting, persistent)\n- Trend graph: playlist requests over last 4 days (requires Nginx Proxy Manager logs)\n- Top referer sites with hit counts (requires Nginx Proxy Manager logs)\n- Worker status with live CPU% (5s polling)\n- Disk space warning (\u003e90%), orphaned upload detection\n\n### Activity Log (`/activity`)\n- Tracks: upload, delete, rename, note, category, poster, re-encode, disable/enable\n- Color-coded action badges, paginated, auto-cleanup after 90 days\n\n### Monitoring\n- `GET /health` — simple health check\n- `GET /health/detailed` — checks database, disk, worker heartbeat, ffmpeg, video counts\n\n### UI/UX\n- Dark/Light/Auto theme toggle\n- German + English (switchable in header)\n- Fire-themed design with flame gradient headings and animated overlays\n- CPU flame overlay during encoding\n- Responsive layout, sticky header\n- Custom HTML dropdowns (no native select elements)\n- SSE (Server-Sent Events) for real-time encoding progress\n\n## Configuration\n\nAll branding and settings are configurable via environment variables. Copy `.env.example` to `.env`:\n\n### Branding\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `BRAND_NAME` | `Eyestream` | Brand name (logo alt text) |\n| `PRODUCT_NAME` | `Eyestream` | Product name (header, title, splash) |\n| `LOGO_URL` | *(empty)* | Logo URL (header, splash screen) |\n| `PUBLIC_BASE` | `https://localhost` | Public CDN URL |\n| `FOOTER_LINKS` | *(empty)* | Footer links, format: `Label1\\|URL1,Label2\\|URL2` |\n| `COPYRIGHT_TEXT` | *(empty)* | Copyright text in footer |\n| `EXAMPLE_PAGE_URL` | *(empty)* | Example page URL (header link, empty = hidden) |\n\n### Database\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `DB_NAME` | `eyestream` | Database name |\n| `DB_USER` | `eyestream` | Database user |\n| `DB_PASSWORD` | `eyestream` | Database password |\n\n### Optional: Reverse Proxy Log Analysis\n\nView counts and referer tracking require Nginx Proxy Manager access logs. Without these, the stats page works but skips the trend graph and referer sections.\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `NPM_SITE_ID` | *(empty)* | NPM proxy host ID (empty = disabled) |\n| `NPM_LOG_HOST_PATH` | `../npm/data/logs` | Host path to NPM log directory |\n| `REFERER_IGNORE_SEEDS` | `localhost` | Comma-separated domains to ignore in referer analysis |\n\n### Authentication\n\nAuthentication is handled by [OAuth2-Proxy](https://github.com/oauth2-proxy/oauth2-proxy). Copy `oauth2-proxy.env.example` to `oauth2-proxy.env` and configure your OIDC provider (Entra ID, Authentik, Keycloak, Google, etc.).\n\n## File Structure\n\n```\napp/\n  main.py              # Entrypoint, lifespan, middleware\n  db.py                # Schema, connection pool\n  helpers.py           # Helper functions, constants\n  routes/\n    videos.py          # Video CRUD, SSE, poster, player proxy\n    settings.py        # Settings, stats, categories\n    misc.py            # Health, monitoring, activity, search, referers\n  templates/           # Jinja2 templates\n  static/\n    base.css           # Variables, header, footer, buttons, pagination\n    components.css     # Video cards, player, overlays, modals\n    pages.css          # Upload, settings, stats pages\n    app.js             # Client-side JavaScript\n\nworker/\n  worker.py            # Encoding pipeline, preview thumbnails, heartbeat\n\nnginx-public/\n  default.conf         # Nginx config with oEmbed endpoint\n  embed.html           # Public embed player with hover previews\n\nconfig/\n  ladder.yml           # Encoding profiles (renditions, bitrates)\n\ntests/                 # Unit + integration tests (55+)\ndocs/                  # Documentation + architecture diagram\n```\n\n## Tests\n\n```bash\npip install -r tests/requirements-test.txt\npytest tests/ -v\n```\n\n## Deployment\n\nFor updates, prefer selective rebuilds over full `docker compose down`:\n\n```bash\ndocker compose up -d --build app worker\n```\n\nThis keeps the public Nginx running — no streaming interruption.\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwvogel%2Feyestream","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwvogel%2Feyestream","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwvogel%2Feyestream/lists"}