c58fa9591b
A retro CRT-themed web TV that channel-surfs live HLS streams through a security-hardened, same-origin Rust relay. Front end (vanilla JS + HLS.js, no framework/build step): - SourceProvider abstraction + canonical Channel/Program model so public streams and future self-hosted media share one UI (MockProvider built first to keep the UI provably source-agnostic; IptvOrgProvider for live data) - Red/black phosphor CRT theme, virtualized 1994-style guide (The Almanac), cable-box zapping with 7-segment OSD, scrying-static transitions - Theater/Dim/Ambient immersion modes, dead-stream "orb clouds over" handling - The Lexicon in-world naming with mundane-mode; full a11y + reduced-motion - HLS.js pinned + Subresource-Integrity verified Rust relay (hyper 1.x + tokio-rustls/ring + hickory-resolver): - Serves the SPA and proxies HLS from one origin (no CORS, untainted canvas) - SSRF defense: resolve -> reject private/loopback/link-local/metadata IPs -> connect to the validated IP (no re-resolve, defeats DNS-rebind), fail-closed - HTTPS-only egress, re-validated redirects, manifest URL rewrite, size/ concurrency caps, strict CSP + security headers, path-traversal guard - /metrics observability + structured logs (host only, never raw URLs) Verified: all SSRF probes rejected, CSP clean, real Apple HLS manifest rewritten, ~39k iptv-org channels fetched end-to-end, tests green, zero warnings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
131 lines
6.1 KiB
HTML
131 lines
6.1 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<title>The Pondering Orb</title>
|
|
|
|
<!-- DEV baseline CSP (meta). The Rust server sends a STRICTER same-origin CSP header
|
|
in production; when both are present the browser enforces the intersection, so
|
|
this loose-for-standalone policy never weakens prod. https: here only lets the
|
|
mock realm play CORS-clean test streams when served by a plain static server. -->
|
|
<meta http-equiv="Content-Security-Policy" content="
|
|
default-src 'self';
|
|
script-src 'self';
|
|
style-src 'self';
|
|
img-src 'self' data: https:;
|
|
media-src 'self' blob: https:;
|
|
connect-src 'self' https:;
|
|
font-src 'self';
|
|
object-src 'none';
|
|
base-uri 'none';
|
|
frame-src 'none';
|
|
form-action 'none'">
|
|
<meta name="referrer" content="no-referrer">
|
|
<meta name="color-scheme" content="dark">
|
|
|
|
<link rel="stylesheet" href="styles.css">
|
|
</head>
|
|
<body>
|
|
|
|
<div id="orb-root" class="orb-root" data-mode-theater="false" data-mode-dim="false" data-mode-ambient="false">
|
|
|
|
<!-- ============ THE SCREEN (the orb) ============ -->
|
|
<div id="screen" class="screen" role="region" aria-label="TV player">
|
|
<canvas id="ambient" class="ambient" aria-hidden="true"></canvas>
|
|
|
|
<div class="bezel">
|
|
<video id="v" class="video" playsinline aria-label="Live TV stream"></video>
|
|
|
|
<!-- scrying static (channel-change transition) -->
|
|
<canvas id="static" class="static" aria-hidden="true"></canvas>
|
|
|
|
<!-- the orb clouds over (dead-stream / no-signal) -->
|
|
<div id="nosignal" class="nosignal" hidden role="status" aria-live="polite">
|
|
<div class="nosignal-bars" aria-hidden="true"></div>
|
|
<div class="nosignal-text" data-lex="noSignal">THE ORB CLOUDS OVER</div>
|
|
<div class="nosignal-sub" data-lex="noSignalSub">no signal — seeking another vision</div>
|
|
</div>
|
|
|
|
<!-- channel banner OSD (now-playing) -->
|
|
<div id="osd-channel" class="osd osd-channel" hidden role="status" aria-live="polite" aria-atomic="true">
|
|
<img id="osd-logo" class="osd-logo" alt="" width="96" height="54">
|
|
<div class="osd-meta">
|
|
<div class="osd-line1"><span id="osd-num" class="osd-num">--</span><span id="osd-name" class="osd-name"></span></div>
|
|
<div id="osd-now" class="osd-now"></div>
|
|
<div class="osd-prog"><span id="osd-prog-fill" class="osd-prog-fill"></span></div>
|
|
</div>
|
|
<div id="osd-live" class="osd-live" hidden><span class="live-dot" aria-hidden="true"></span><span data-lex="live">LIVE</span></div>
|
|
</div>
|
|
|
|
<!-- incantation (number entry) OSD -->
|
|
<div id="osd-number" class="osd osd-number" hidden role="status" aria-live="assertive" aria-atomic="true">
|
|
<span id="osd-number-val" class="seg">---</span>
|
|
</div>
|
|
|
|
<!-- ghost controls -->
|
|
<div id="controls" class="controls" role="toolbar" aria-label="Player controls">
|
|
<button id="btn-play" class="ctl" aria-label="Play/Pause" aria-pressed="false">▶</button>
|
|
<button id="btn-chdn" class="ctl" aria-label="Channel down">CH ▼</button>
|
|
<button id="btn-chup" class="ctl" aria-label="Channel up">CH ▲</button>
|
|
<div class="ctl-live" id="ctl-live-wrap" hidden><span class="live-dot" aria-hidden="true"></span><span data-lex="live">LIVE</span></div>
|
|
<span id="ctl-clock" class="ctl-clock" aria-hidden="true">--:--</span>
|
|
<span class="ctl-spacer"></span>
|
|
<button id="btn-mute" class="ctl" aria-label="Mute" aria-pressed="false">🔊</button>
|
|
<input id="vol" class="vol" type="range" min="0" max="100" value="100" aria-label="Volume">
|
|
<button id="btn-cc" class="ctl" aria-label="Captions" aria-pressed="false">CC</button>
|
|
<button id="btn-qual" class="ctl" aria-label="Quality">AUTO</button>
|
|
<button id="btn-pip" class="ctl" aria-label="Picture in picture" hidden>PiP</button>
|
|
<button id="btn-guide" class="ctl" aria-label="TV guide" data-lex="guideShort">Almanac</button>
|
|
<button id="btn-full" class="ctl" aria-label="Fullscreen">⛶</button>
|
|
</div>
|
|
|
|
<!-- quality menu -->
|
|
<div id="qual-menu" class="qual-menu" hidden role="menu" aria-label="Quality"></div>
|
|
|
|
<!-- CRT overlays (scanlines / vignette) -->
|
|
<div class="crt-scan" aria-hidden="true"></div>
|
|
<div class="crt-vignette" aria-hidden="true"></div>
|
|
</div>
|
|
|
|
<div class="dim-scrim" id="dim-scrim" aria-hidden="true"></div>
|
|
</div>
|
|
|
|
<!-- ============ THE ALMANAC (guide) ============ -->
|
|
<section id="guide" class="guide" hidden aria-label="TV Guide" aria-hidden="true">
|
|
<header class="guide-head">
|
|
<h2 class="guide-title" data-lex="guide">The Almanac</h2>
|
|
<div class="guide-clock" id="guide-clock">--:--</div>
|
|
<button id="guide-close" class="ctl" aria-label="Close guide">✕</button>
|
|
</header>
|
|
<div class="guide-scroll" id="guide-scroll" role="grid" aria-label="Channel schedule" tabindex="0">
|
|
<div class="guide-timehdr" id="guide-timehdr" role="row" aria-hidden="true"></div>
|
|
<div class="guide-rows" id="guide-rows"></div>
|
|
<div class="guide-playhead" id="guide-playhead" aria-hidden="true"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============ INCANTATIONS (keyboard help) ============ -->
|
|
<div id="help" class="help" hidden role="dialog" aria-modal="true" aria-label="Keyboard shortcuts">
|
|
<div class="help-card">
|
|
<h2 data-lex="help">Incantations</h2>
|
|
<dl id="help-list"></dl>
|
|
<button id="help-close" class="ctl" aria-label="Close">close (Esc)</button>
|
|
<label class="mundane-toggle">
|
|
<input type="checkbox" id="mundane"> plain-English labels (mundane mode)
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- brand bug -->
|
|
<div class="brand-bug" aria-hidden="true" data-lex="brand">THE PONDERING ORB</div>
|
|
</div>
|
|
|
|
<!-- HLS engine: local, pinned (v1.5.17), SRI-verified. -->
|
|
<script src="lib/hls.min.js"
|
|
integrity="sha384-9v3HcdYrO3D+OPDTjZ40RXocgE4GtXVCd3/mCS62JsM93JXgI1afJVuwjFvsu6ni"
|
|
crossorigin="anonymous"></script>
|
|
<script type="module" src="app.js"></script>
|
|
</body>
|
|
</html>
|