diff --git a/apps/web/app.js b/apps/web/app.js index b3f100b..17f2502 100644 --- a/apps/web/app.js +++ b/apps/web/app.js @@ -110,19 +110,19 @@ function bindPlayerSignals(watch, name, extraCleanup) { if (status === "loading") { clearOfflineTimer(); sawLoading = true; - setHint(`Connecting to relay and subscribing: ${name}`, "ok"); + setHint(`Tuning ${name}...`, "ok"); return; } if (status === "live") { clearOfflineTimer(); - setHint(`Live: subscribed to ${name}`, "ok"); + setHint(`On air: ${name}`, "ok"); return; } if (status === "offline" && sawLoading) { clearOfflineTimer(); // Avoid flashing a false negative during short reconnect windows. offlineTimer = window.setTimeout(() => { - setHint(`Connection interrupted, retrying: ${name}`, "warn"); + setHint(`Signal faded, retrying ${name}...`, "warn"); offlineTimer = null; }, 8000); } @@ -133,7 +133,7 @@ function bindPlayerSignals(watch, name, extraCleanup) { const hasVideo = Boolean(catalog.video && catalog.video.renditions); const hasAudio = Boolean(catalog.audio && catalog.audio.renditions); if (hasVideo || hasAudio) { - setHint(`Live: subscribed to ${name}`, "ok"); + setHint(`On air: ${name}`, "ok"); } }); @@ -197,7 +197,7 @@ function mountPlayer(relayUrl, name) { forceAudioOn(); watch.backend?.paused?.set?.(true); watch.backend?.paused?.set?.(false); - setHint(`Live: subscribed to ${name} (audio unlocked)`, "ok"); + setHint(`On air: ${name} (sound on)`, "ok"); }; document.addEventListener("pointerdown", unlockAudio, { once: true }); canvas.addEventListener("pointerdown", unlockAudio, { once: true }); @@ -205,7 +205,7 @@ function mountPlayer(relayUrl, name) { document.removeEventListener("pointerdown", unlockAudio); canvas.removeEventListener("pointerdown", unlockAudio); }); - setHint(`Live: subscribed to ${name} (tap player to unmute)`, "warn"); + setHint(`On air: ${name} (tap picture for sound)`, "warn"); bindPlayerSignals(watch, name, cleanup); } @@ -285,7 +285,7 @@ async function mountArchivePlayer(name) { video.playsInline = true; video.addEventListener("error", () => { setHint( - "Archive replay bytes are not browser-HLS compatible yet (legacy container); timeline is available, live path is unaffected.", + "This replay is not ready for browser playback yet. Live tuning is unaffected.", "warn", ); }); @@ -312,7 +312,7 @@ async function mountArchivePlayer(name) { hls.on(HlsCtor.Events.ERROR, (_event, data) => { if (!data?.fatal) return; if (data.type === HlsCtor.ErrorTypes.NETWORK_ERROR) { - setHint("Archive network hiccup, retrying…", "warn"); + setHint("Replay hiccup, retrying...", "warn"); try { hls.startLoad(); } catch (_) { @@ -321,7 +321,7 @@ async function mountArchivePlayer(name) { return; } if (data.type === HlsCtor.ErrorTypes.MEDIA_ERROR) { - setHint("Archive media hiccup, recovering…", "warn"); + setHint("Replay picture hiccup, recovering...", "warn"); try { hls.recoverMediaError(); } catch (_) { @@ -329,7 +329,7 @@ async function mountArchivePlayer(name) { } return; } - setHint(`Archive playback error: ${data.type || "fatal"}`, "warn"); + setHint(`Replay unavailable: ${data.type || "fatal"}`, "warn"); }); hls.loadSource(archiveUrl); hls.attachMedia(video); @@ -392,10 +392,10 @@ function renderLiveList(entries, onWatchLive, onWatchArchive) { const mount = $("liveList"); mount.textContent = ""; if (!entries.length) { - setListHint("No public streams announced yet.", ""); + setListHint("No stations are on air yet.", ""); return; } - setListHint(`${entries.length} live`, "ok"); + setListHint(`${entries.length} on air`, "ok"); for (const entry of entries) { const row = document.createElement("div"); @@ -409,7 +409,9 @@ function renderLiveList(entries, onWatchLive, onWatchArchive) { const meta = document.createElement("div"); meta.className = "liveMeta"; - meta.textContent = `${entry.broadcast_name || ""} @ ${entry.relay_url || DEFAULT_RELAY_URL}`; + meta.textContent = entry.broadcast_name + ? `Channel key: ${entry.broadcast_name}` + : "Ready to tune"; info.appendChild(meta); const actions = document.createElement("div"); @@ -417,14 +419,14 @@ function renderLiveList(entries, onWatchLive, onWatchArchive) { const watchBtn = document.createElement("button"); watchBtn.className = "btn secondary"; - watchBtn.textContent = "Live"; + watchBtn.textContent = "Watch"; watchBtn.addEventListener("click", () => { onWatchLive(entry); }); const archiveBtn = document.createElement("button"); archiveBtn.className = "btn secondary"; - archiveBtn.textContent = "Archive"; + archiveBtn.textContent = "DVR"; archiveBtn.addEventListener("click", () => { onWatchArchive(entry); }); @@ -480,18 +482,18 @@ function main() { updateSharePreview(); if (!name) { - setHint("Enter a broadcast name to watch.", "warn"); + setHint("Pick a station or enter a channel key.", "warn"); return; } if (mode === "archive") { writeParams(relayUrl, name, mode); - setHint(`Loading archive DVR: ${name}`, "ok"); + setHint(`Loading replay: ${name}`, "ok"); try { await mountArchivePlayer(name); } catch (e) { setHint( - `Archive playback unavailable: ${String(e)}. Ensure /api/archive is configured.`, + `Replay unavailable: ${String(e)}.`, "warn", ); } @@ -500,7 +502,7 @@ function main() { if (!hasWebTransport()) { setHint( - "WebTransport is not available in this browser. Try Chrome or Firefox Nightly. Safari support is still incomplete.", + "This browser cannot tune the live player yet. Try Chrome or Edge.", "warn", ); return; @@ -510,14 +512,14 @@ function main() { await ensureMoqWatchElement(); } catch (e) { setHint( - `Failed to load MoQ web player dependency: ${String(e)}. Disable script blockers for esm.sh/jsdelivr/unpkg and retry.`, + `The player did not load: ${String(e)}. Try again after allowing the page scripts.`, "warn", ); return; } writeParams(relayUrl, name, mode); - setHint(`Connecting to relay and subscribing: ${name}`, "ok"); + setHint(`Tuning ${name}...`, "ok"); mountPlayer(relayUrl, name); } @@ -537,7 +539,7 @@ function main() { const name = normalizeName(nameInput.value); const mode = archiveModeInput.checked ? "archive" : "live"; if (!name) { - setHint("Enter a broadcast name first.", "warn"); + setHint("Pick a station first.", "warn"); return; } const link = currentShareLink(relayUrl, name, mode); @@ -552,7 +554,7 @@ function main() { }); async function refreshLiveList() { - setListHint("Loading live streams...", ""); + setListHint("Scanning stations...", ""); try { const entries = await fetchLiveList(); renderLiveList( @@ -574,7 +576,7 @@ function main() { ); } catch (e) { $("liveList").textContent = ""; - setListHint(`Live list error: ${String(e)}`, "warn"); + setListHint("Station guide is unavailable right now.", "warn"); } } refreshListBtn.addEventListener("click", () => { diff --git a/apps/web/assets/sony-master-control.png b/apps/web/assets/sony-master-control.png new file mode 100644 index 0000000..0a9d3aa Binary files /dev/null and b/apps/web/assets/sony-master-control.png differ diff --git a/apps/web/index.html b/apps/web/index.html index 5f3702e..0faba97 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -10,70 +10,72 @@ /> +
-
-
-
every.channel
-
Watch live streams over WebTransport.
-
-
- WebTransport -
-
- -
-
Watch
-
- - - - -
-
- -
- -
-
-
Live Now
- -
-
-
-
- -
-
-
-
-
- +
+
+
+
every.channel
+
Live television, tuned from real signals.
-
+
Local broadcast
+ + +
+
+
+
+
+ +
+
+
+
+ + +
diff --git a/apps/web/style.css b/apps/web/style.css index 49c941a..325abbb 100644 --- a/apps/web/style.css +++ b/apps/web/style.css @@ -1,17 +1,18 @@ :root { - --bg0: #0b0f14; - --bg1: #0f1720; - --panel: rgba(255, 255, 255, 0.06); - --panel2: rgba(255, 255, 255, 0.08); - --text: rgba(255, 255, 255, 0.92); - --muted: rgba(255, 255, 255, 0.65); - --faint: rgba(255, 255, 255, 0.45); - --line: rgba(255, 255, 255, 0.12); - --accent: #ffb86c; - --accent2: #6ee7ff; - --ok: #7cf7a2; - --warn: #ffd36e; - --shadow: rgba(0, 0, 0, 0.55); + --bg: #050505; + --ink: rgba(255, 248, 235, 0.96); + --muted: rgba(255, 248, 235, 0.70); + --faint: rgba(255, 248, 235, 0.48); + --line: rgba(255, 248, 235, 0.18); + --panel: rgba(10, 9, 7, 0.74); + --panel-strong: rgba(6, 6, 5, 0.88); + --button: rgba(255, 248, 235, 0.10); + --button-hover: rgba(255, 248, 235, 0.16); + --amber: #f1a94f; + --green: #79e28b; + --red: #f06958; + --blue: #7eb6d8; + --shadow: rgba(0, 0, 0, 0.68); } * { @@ -20,190 +21,215 @@ html, body { - height: 100%; + min-height: 100%; } body { margin: 0; - color: var(--text); + color: var(--ink); + background: var(--bg); + font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; +} + +body::before { + content: ""; + position: fixed; + inset: 0; + z-index: -2; + background-image: url("assets/sony-master-control.png"); + background-position: center top; + background-size: cover; +} + +body::after { + content: ""; + position: fixed; + inset: 0; + z-index: -1; background: - radial-gradient(1200px 700px at 15% 10%, rgba(255, 184, 108, 0.16), transparent 55%), - radial-gradient(900px 600px at 85% 20%, rgba(110, 231, 255, 0.12), transparent 60%), - radial-gradient(900px 900px at 50% 120%, rgba(255, 255, 255, 0.08), transparent 55%), - linear-gradient(180deg, var(--bg0), var(--bg1)); - font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", - "Segoe UI Emoji"; + linear-gradient(90deg, rgba(0, 0, 0, 0.58), rgba(0, 0, 0, 0.12) 48%, rgba(0, 0, 0, 0.66)), + linear-gradient(180deg, rgba(0, 0, 0, 0.24), rgba(0, 0, 0, 0.78) 82%, rgba(0, 0, 0, 0.95)); } .shell { - max-width: 1100px; + width: min(1220px, calc(100% - 32px)); + min-height: 100vh; margin: 0 auto; - padding: 28px 18px 22px; display: grid; - gap: 16px; + grid-template-rows: 1fr auto; + gap: 14px; + padding: 22px 0 14px; } -.top { +.studio { + min-height: calc(100vh - 58px); + display: grid; + grid-template-rows: auto 1fr; + gap: 18px; +} + +.masthead { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; - gap: 16px; - padding: 14px 16px; - border: 1px solid var(--line); - border-radius: 16px; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.04)); - box-shadow: 0 18px 38px var(--shadow); + gap: 18px; + padding-top: 4px; } .brand-title { + font-size: 34px; + line-height: 1; font-weight: 800; - letter-spacing: -0.02em; - font-size: 22px; - line-height: 1.05; + letter-spacing: 0; + text-shadow: 0 2px 22px rgba(0, 0, 0, 0.85); } .brand-subtitle { - margin-top: 4px; + margin-top: 8px; + max-width: 420px; color: var(--muted); - font-size: 13px; + font-size: 15px; + line-height: 1.35; + text-shadow: 0 1px 18px rgba(0, 0, 0, 0.9); } .badge { - padding: 8px 10px; - font-size: 12px; - color: rgba(0, 0, 0, 0.78); - background: linear-gradient(135deg, var(--accent), var(--accent2)); - border-radius: 999px; - font-weight: 700; -} - -.panel { - padding: 14px 16px; - border: 1px solid var(--line); - border-radius: 16px; - background: var(--panel); - box-shadow: 0 18px 38px var(--shadow); -} - -.panel-title { - font-size: 12px; - letter-spacing: 0.22em; - text-transform: uppercase; - color: var(--muted); - margin-bottom: 12px; -} - -.panelHead { - display: flex; + min-height: 34px; + display: inline-flex; align-items: center; - justify-content: space-between; - gap: 10px; + padding: 0 12px; + border: 1px solid rgba(121, 226, 139, 0.48); + border-radius: 6px; + color: rgba(236, 255, 232, 0.95); + background: rgba(17, 44, 22, 0.58); + font-size: 12px; + font-weight: 800; } -.panelHead .panel-title { - margin-bottom: 0; -} - -.row { +.watchDeck { + align-self: end; display: grid; - grid-template-columns: 1.15fr 1fr auto auto; - gap: 10px; + grid-template-columns: minmax(0, 1fr) 356px; + gap: 18px; align-items: end; } -.field .label { - font-size: 12px; - color: var(--muted); - margin-bottom: 6px; +.player { + min-width: 0; } -.input { +.tv { + padding: 12px; + border-radius: 8px; + background: + linear-gradient(180deg, rgba(24, 23, 20, 0.96), rgba(7, 7, 6, 0.98)), + #090908; + border: 1px solid rgba(255, 248, 235, 0.16); + box-shadow: + 0 30px 70px var(--shadow), + inset 0 1px 0 rgba(255, 255, 255, 0.12); +} + +.tv-frame { + position: relative; + padding: 10px; + border-radius: 6px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(0, 0, 0, 0.35)), + #050505; + border: 1px solid rgba(255, 248, 235, 0.12); +} + +.mount { width: 100%; - padding: 11px 12px; + aspect-ratio: 16 / 9; + overflow: hidden; + border-radius: 4px; + background: #000; + border: 1px solid rgba(255, 248, 235, 0.20); +} + +.canvas, +.archiveVideo { + width: 100%; + height: 100%; + display: block; + background: #000; +} + +.tv-scanlines { + position: absolute; + inset: 10px; + border-radius: 4px; + background: repeating-linear-gradient( + to bottom, + rgba(255, 255, 255, 0.025), + rgba(255, 255, 255, 0.025) 1px, + rgba(0, 0, 0, 0.00) 3px, + rgba(0, 0, 0, 0.00) 6px + ); + mix-blend-mode: overlay; + pointer-events: none; + opacity: 0.35; +} + +.console { + padding: 14px; + border-radius: 8px; + background: + linear-gradient(180deg, rgba(255, 248, 235, 0.08), rgba(255, 248, 235, 0.03)), + var(--panel); border: 1px solid var(--line); - border-radius: 12px; - background: rgba(0, 0, 0, 0.28); - color: var(--text); - outline: none; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); + box-shadow: 0 30px 68px var(--shadow); + backdrop-filter: blur(18px); } -.input:focus { - border-color: rgba(255, 184, 108, 0.55); - box-shadow: 0 0 0 3px rgba(255, 184, 108, 0.16); -} - -.checkField .checkRow { +.consoleHead { display: flex; - align-items: center; - gap: 8px; - min-height: 44px; - padding: 0 10px; - border: 1px solid var(--line); - border-radius: 12px; - background: rgba(0, 0, 0, 0.28); - color: var(--text); + align-items: flex-start; + justify-content: space-between; + gap: 12px; } -.checkField input[type="checkbox"] { - width: 16px; - height: 16px; +.panel-title { + margin: 0; + color: var(--amber); + font-size: 12px; + font-weight: 800; + letter-spacing: 0; + text-transform: uppercase; } -.btn { - padding: 11px 14px; - border-radius: 12px; - border: 1px solid rgba(255, 184, 108, 0.35); - background: linear-gradient(180deg, rgba(255, 184, 108, 0.22), rgba(255, 184, 108, 0.12)); - color: var(--text); - font-weight: 700; - cursor: pointer; - transition: transform 80ms ease, background 120ms ease, border-color 120ms ease; -} - -.btn:hover { - transform: translateY(-1px); - border-color: rgba(255, 184, 108, 0.55); - background: linear-gradient(180deg, rgba(255, 184, 108, 0.28), rgba(255, 184, 108, 0.14)); -} - -.btn:active { - transform: translateY(0); -} - -.btn.secondary { - border-color: rgba(255, 255, 255, 0.18); - background: rgba(255, 255, 255, 0.06); +.consoleCopy { + margin-top: 4px; + color: var(--muted); + font-size: 13px; + line-height: 1.35; } .hint { + min-height: 20px; margin-top: 10px; - font-size: 13px; color: var(--muted); - min-height: 18px; + font-size: 13px; + line-height: 1.35; +} + +.player > .hint { + margin-left: 2px; + text-shadow: 0 1px 14px rgba(0, 0, 0, 0.9); } .hint[data-kind="ok"] { - color: var(--ok); + color: var(--green); } .hint[data-kind="warn"] { - color: var(--warn); + color: #ffd17a; } -.share { - margin-top: 8px; - grid-template-columns: auto 1fr; - align-items: center; -} - -.shareLink { - color: var(--faint); - font-size: 12px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding: 0 4px; +.listHint { + margin-top: 12px; } .liveList { @@ -215,22 +241,44 @@ body { .liveItem { display: grid; - grid-template-columns: 1fr auto; - gap: 8px; + grid-template-columns: 10px minmax(0, 1fr) auto; align-items: center; - padding: 9px 10px; - border-radius: 10px; - border: 1px solid var(--line); - background: rgba(0, 0, 0, 0.18); + gap: 10px; + min-height: 58px; + padding: 10px; + border: 1px solid rgba(255, 248, 235, 0.14); + border-radius: 6px; + background: rgba(0, 0, 0, 0.34); +} + +.liveItem::before { + content: ""; + grid-column: 1; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--red); + box-shadow: 0 0 12px rgba(240, 105, 88, 0.85); + align-self: center; +} + +.liveItem > div:first-child { + grid-column: 2; + min-width: 0; +} + +.liveActions { + grid-column: 3; } .liveTitle { font-size: 14px; - font-weight: 600; + font-weight: 800; + line-height: 1.2; } .liveMeta { - margin-top: 2px; + margin-top: 3px; color: var(--faint); font-size: 12px; overflow: hidden; @@ -243,100 +291,186 @@ body { gap: 6px; } -.player { - padding: 0; +.signalDrawer { + margin-top: 14px; + border-top: 1px solid rgba(255, 248, 235, 0.12); + padding-top: 12px; } -.tv { - position: relative; - border-radius: 24px; - padding: 18px; - border: 1px solid rgba(255, 255, 255, 0.14); - background: - radial-gradient(900px 500px at 10% 10%, rgba(255, 184, 108, 0.10), transparent 55%), - radial-gradient(700px 500px at 90% 10%, rgba(110, 231, 255, 0.08), transparent 58%), - linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(0, 0, 0, 0.10)); - box-shadow: 0 28px 70px rgba(0, 0, 0, 0.62); +.signalDrawer summary { + cursor: pointer; + color: var(--muted); + font-size: 13px; + font-weight: 800; + list-style-position: outside; } -.tv-glow { - position: absolute; - inset: -40px -20px -20px -20px; - background: - radial-gradient(350px 220px at 18% 22%, rgba(255, 184, 108, 0.18), transparent 70%), - radial-gradient(320px 220px at 82% 26%, rgba(110, 231, 255, 0.14), transparent 72%); - filter: blur(18px); - opacity: 0.95; - pointer-events: none; +.signalGrid { + display: grid; + gap: 10px; + margin-top: 12px; } -.tv-frame { - position: relative; - border-radius: 18px; - padding: 12px; - background: - linear-gradient(180deg, rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0.22)); - border: 1px solid rgba(255, 255, 255, 0.12); +.field { + display: grid; + gap: 6px; } -.mount { - aspect-ratio: 16 / 9; +.label { + color: var(--faint); + font-size: 12px; +} + +.input { width: 100%; - border-radius: 12px; + min-height: 42px; + padding: 10px 11px; + color: var(--ink); + background: rgba(0, 0, 0, 0.42); + border: 1px solid rgba(255, 248, 235, 0.17); + border-radius: 6px; + outline: none; +} + +.input:focus { + border-color: rgba(241, 169, 79, 0.72); + box-shadow: 0 0 0 3px rgba(241, 169, 79, 0.16); +} + +.checkRow { + min-height: 42px; + display: flex; + align-items: center; + gap: 8px; + padding: 0 10px; + color: var(--muted); + background: rgba(0, 0, 0, 0.35); + border: 1px solid rgba(255, 248, 235, 0.14); + border-radius: 6px; +} + +.checkRow input { + width: 16px; + height: 16px; +} + +.btn { + min-height: 38px; + padding: 9px 12px; + border-radius: 6px; + border: 1px solid rgba(241, 169, 79, 0.48); + color: var(--ink); + background: + linear-gradient(180deg, rgba(241, 169, 79, 0.28), rgba(141, 82, 26, 0.26)), + var(--button); + font-weight: 800; + cursor: pointer; + transition: transform 80ms ease, border-color 120ms ease, background 120ms ease; +} + +.btn:hover { + transform: translateY(-1px); + border-color: rgba(241, 169, 79, 0.72); + background: + linear-gradient(180deg, rgba(241, 169, 79, 0.34), rgba(141, 82, 26, 0.30)), + var(--button-hover); +} + +.btn:active { + transform: translateY(0); +} + +.btn.secondary { + border-color: rgba(255, 248, 235, 0.18); + background: rgba(255, 248, 235, 0.09); +} + +.share { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 8px; + align-items: center; + margin-top: 10px; +} + +.shareLink { + min-width: 0; overflow: hidden; - background: #000; - border: 1px solid rgba(255, 255, 255, 0.14); -} - -.canvas { - width: 100%; - height: 100%; - display: block; -} - -.archiveVideo { - width: 100%; - height: 100%; - display: block; - background: #000; -} - -.tv-scanlines { - position: absolute; - inset: 12px; - border-radius: 12px; - background: repeating-linear-gradient( - to bottom, - rgba(255, 255, 255, 0.03), - rgba(255, 255, 255, 0.03) 1px, - rgba(0, 0, 0, 0.00) 3px, - rgba(0, 0, 0, 0.00) 6px - ); - mix-blend-mode: overlay; - pointer-events: none; - opacity: 0.25; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--faint); + font-size: 12px; } .foot { display: flex; justify-content: space-between; gap: 12px; - padding: 10px 2px 0; - color: var(--faint); + color: rgba(255, 248, 235, 0.48); font-size: 12px; } -@media (max-width: 900px) { - .row { - grid-template-columns: 1fr; - align-items: stretch; +@media (max-width: 980px) { + body::before { + background-position: center top; } - .share { - grid-template-columns: 1fr; + + .shell { + width: min(100% - 24px, 760px); + padding-top: 16px; } + + .watchDeck { + grid-template-columns: 1fr; + align-self: start; + } + + .console { + order: -1; + } +} + +@media (max-width: 620px) { + .masthead { + align-items: flex-start; + } + + .brand-title { + font-size: 28px; + } + .badge { display: none; } + + .tv, + .console { + padding: 10px; + } + + .liveItem { + grid-template-columns: minmax(0, 1fr); + } + + .liveItem::before { + display: none; + } + + .liveItem > div:first-child, + .liveActions { + grid-column: auto; + } + + .liveActions, + .share { + grid-template-columns: 1fr 1fr; + width: 100%; + } + + .liveActions { + display: grid; + } + .foot { flex-direction: column; } diff --git a/evolution/proposals/ECP-0118-broadcast-console-web-presentation.md b/evolution/proposals/ECP-0118-broadcast-console-web-presentation.md new file mode 100644 index 0000000..c9c73b0 --- /dev/null +++ b/evolution/proposals/ECP-0118-broadcast-console-web-presentation.md @@ -0,0 +1,31 @@ +# ECP-0118: Broadcast Console Web Presentation + +Status: Draft + +## Problem / context + +The hosted web page presented the live player as a technical protocol console: relay URL, broadcast +name, WebTransport badge, and diagnostic wording were all first-screen material. That is useful for +operators, but it makes the public experience feel like infrastructure instead of television. + +## Decision + +Use a generated 1980s broadcast master-control image as the page backdrop and redesign the first +screen around a tuned television surface plus a compact station console. Keep the live player and +public stream list visible, but move relay URL, channel key, DVR mode, and share URL into a +collapsed "Signal source" drawer. + +## Consequences + +- The first impression is a premium broadcast console instead of a protocol dashboard. +- Station discovery remains one click from the first screen. +- Operator controls are still present for debugging and direct links, but no longer dominate the + public page. +- The generated image is unbranded and contains no readable labels, so it evokes the Sony-era + hardware direction without making a visible trademark claim. + +## Rollout / teardown + +Deploy the web asset bundle and validate the hosted page visually in the browser plus the existing +watch E2E. Teardown is removing the generated asset, restoring the prior panel layout, and promoting +the signal fields back to the first-screen controls.