diff --git a/README.md b/README.md index 2c8dd4b..e52bdbd 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,16 @@ All processes are managed by Supervisord. The web UI is a Next.js app that contr - **Stream any URL** — if it loads in a browser, it streams - **Dashboard with live thumbnails** — captured directly from the Xvfb display, refreshable on demand +- **Scalable card sizes** — mini/sm/md/lg sizes scale all card elements proportionally (buttons, text, icons, padding) - **Inline VNC** — inspect any stream's virtual display without leaving the UI (`/vnc/{id}`) - **Autologin with CDP detection** — configure credentials per stream; on restart, queries Chrome DevTools Protocol to skip login if the session is still alive - **Persistent desired state** — streams remember if they were running or stopped and restore automatically on container restart - **Optional authentication** — set `AUTH_USER` + `AUTH_PASS` to password-protect the entire UI; rolling 30-day session, no login required while active - **Fully configurable encoding** — resolution, scale, FPS, bitrate, preset, tune, GOP, threads, all per stream - **GPU acceleration** — optional per-stream Chromium GPU flag (disabled by default for container compatibility) -- **Built-in HLS player** — watch any stream in the browser; also serves a standalone embeddable HTML page per stream +- **Built-in HLS player** — watch any stream in the browser via a standalone HTML page optimized for TVs (Back + Mute buttons, reconnect on stall, direct MediaMTX connection when available) +- **Per-card Pure mode** — toggle in the card menu to open Play Stream as a raw `.m3u8` link or Run HTML as a minimal `.html` page with no UI; works with native players and TV browsers +- **Per-card new tab** — toggle to open any button in a new tab instead of navigating in place; both settings are per-card and saved in the browser ## Platform Support @@ -103,9 +106,13 @@ Each stream gets a slug ID you define (e.g. `grafana-prod`): | Protocol | URL | |----------|-----| | RTMP ingest | `rtmp://:1935/live/` | -| HLS manifest | `http://:3000/api/hls/live//index.m3u8` | +| HLS manifest (proxied) | `http://:3000/api/hls/live//index.m3u8` | +| HLS manifest (direct) | `http://:8888/live//index.m3u8` — requires port 8888 exposed | +| HTML player | `http://:3000/player/.html` — minimal page, no UI chrome | | VNC (inline) | `http://:3000/vnc/` | +> **Pure mode** (toggle per card): Play Stream opens the proxied HLS `.m3u8` directly; Run HTML opens the `.html` player. Both can be pasted into VLC or any HLS-capable player, or loaded natively on TV browsers that support HLS. + ## Stream Configuration | Field | Default | Description | diff --git a/next.config.ts b/next.config.ts index de2d453..eab337e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,14 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", + async rewrites() { + return [ + { + source: "/player/:id.html", + destination: "/api/player-html/:id", + }, + ] + }, }; export default nextConfig; \ No newline at end of file diff --git a/scripts/stream.template.conf b/scripts/stream.template.conf index bae9c08..e01a11d 100644 --- a/scripts/stream.template.conf +++ b/scripts/stream.template.conf @@ -27,7 +27,7 @@ command=bash -c "rm -rf \ --disable-background-timer-throttling \ --remote-debugging-port={{DEBUG_PORT}} \ --password-store=basic \ - --disable-features=PasswordManagerRedesign,PasswordSuggestions \ + --disable-features=PasswordManagerRedesign,PasswordSuggestions,Translate \ '{{STREAM_URL}}'" environment=DISPLAY={{DISPLAY}} autorestart=true diff --git a/src/app/api/hls/[...path]/route.ts b/src/app/api/hls/[...path]/route.ts index 4d1b9d2..351f1a7 100644 --- a/src/app/api/hls/[...path]/route.ts +++ b/src/app/api/hls/[...path]/route.ts @@ -16,7 +16,15 @@ export async function GET(req: NextRequest, { params }: Ctx) { const headers = new Headers() const ct = res.headers.get("content-type") if (ct) headers.set("content-type", ct) - headers.set("cache-control", "no-cache") + + const cl = res.headers.get("content-length") + if (cl) headers.set("content-length", cl) + const ar = res.headers.get("accept-ranges") + if (ar) headers.set("accept-ranges", ar) + + // .ts segments are immutable — cache them; playlists must stay fresh + const isSegment = path[path.length - 1]?.endsWith(".ts") + headers.set("cache-control", isSegment ? "public, max-age=300, immutable" : "no-cache, no-store") return new NextResponse(res.body, { status: 200, headers }) } catch { diff --git a/src/app/player-static/[id]/route.ts b/src/app/api/player-html/[id]/route.ts similarity index 74% rename from src/app/player-static/[id]/route.ts rename to src/app/api/player-html/[id]/route.ts index fda5f13..2b73fad 100644 --- a/src/app/player-static/[id]/route.ts +++ b/src/app/api/player-html/[id]/route.ts @@ -26,14 +26,20 @@ export async function GET(_req: NextRequest, { params }: Ctx) {
` @@ -61,4 +71,4 @@ export async function GET(_req: NextRequest, { params }: Ctx) { return new NextResponse(html, { headers: { "Content-Type": "text/html; charset=utf-8" }, }) -} \ No newline at end of file +} diff --git a/src/app/player/[id]/page.tsx b/src/app/player/[id]/page.tsx index 53b4c2d..2eef0e6 100644 --- a/src/app/player/[id]/page.tsx +++ b/src/app/player/[id]/page.tsx @@ -129,7 +129,7 @@ function PlayerInner() {
router.push("/")} /> {mode === "hls" && } - {mode === "html" &&