// every.channel web watcher // // This uses the upstream hang web component (WebTransport + WebCodecs). // It is intentionally dependency-light: no framework, no bundler. const DEFAULT_RELAY_URL = "https://interop-relay.cloudflare.mediaoverquic.com/"; const HANG_WATCH_MODULE_URL = "https://esm.sh/@kixelated/hang@0.7.4/watch/element.js"; let hangWatchModulePromise = null; function $(id) { const el = document.getElementById(id); if (!el) throw new Error(`missing element: ${id}`); return el; } function normalizeRelayUrl(s) { const trimmed = (s || "").trim(); if (!trimmed) return DEFAULT_RELAY_URL; // Ensure trailing slash so relative fetches behave consistently. return trimmed.endsWith("/") ? trimmed : `${trimmed}/`; } function normalizeName(s) { return (s || "").trim(); } function currentShareLink(relayUrl, name) { const u = new URL(window.location.href); u.pathname = "/watch"; u.searchParams.set("url", relayUrl); u.searchParams.set("name", name); // Avoid leaking other params. for (const k of [...u.searchParams.keys()]) { if (k !== "url" && k !== "name") u.searchParams.delete(k); } return u.toString(); } function setHint(text, kind) { const el = $("hint"); el.textContent = text || ""; el.dataset.kind = kind || ""; } function setShareLink(text) { const el = $("shareLink"); el.textContent = text || ""; } function mountPlayer(relayUrl, name) { const mount = $("playerMount"); mount.textContent = ""; const watch = document.createElement("hang-watch"); watch.setAttribute("url", relayUrl); watch.setAttribute("name", name); watch.setAttribute("controls", ""); // A canvas enables video rendering. Without it, only audio is played. const canvas = document.createElement("canvas"); canvas.className = "canvas"; watch.appendChild(canvas); mount.appendChild(watch); } async function ensureHangWatchElement() { if (window.customElements && window.customElements.get("hang-watch")) return; if (!hangWatchModulePromise) { hangWatchModulePromise = import(HANG_WATCH_MODULE_URL); } await hangWatchModulePromise; if (!(window.customElements && window.customElements.get("hang-watch"))) { throw new Error("hang-watch custom element is unavailable"); } } async function copyToClipboard(text) { if (!text) return; if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); return; } // Fallback: best-effort. const ta = document.createElement("textarea"); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); } function readParams() { const u = new URL(window.location.href); // Accept legacy/share-link aliases for compatibility. const relay = u.searchParams.get("url") || u.searchParams.get("relay") || u.searchParams.get("relayUrl"); const name = u.searchParams.get("name") || u.searchParams.get("broadcast"); return { relayUrl: normalizeRelayUrl(relay || DEFAULT_RELAY_URL), name: normalizeName(name || ""), }; } function writeParams(relayUrl, name) { const u = new URL(window.location.href); u.pathname = "/watch"; u.searchParams.set("url", relayUrl); u.searchParams.set("name", name); // Canonicalize by dropping stale aliases/extra params. for (const k of [...u.searchParams.keys()]) { if (k !== "url" && k !== "name") u.searchParams.delete(k); } window.history.replaceState({}, "", u.toString()); } function hasWebTransport() { return typeof window.WebTransport !== "undefined"; } function main() { const relayInput = $("relayUrl"); const nameInput = $("broadcastName"); const watchBtn = $("watchBtn"); const copyBtn = $("copyLinkBtn"); const initial = readParams(); relayInput.value = initial.relayUrl; nameInput.value = initial.name; function updateSharePreview() { const relayUrl = normalizeRelayUrl(relayInput.value); const name = normalizeName(nameInput.value); if (!name) { setShareLink(""); return; } setShareLink(currentShareLink(relayUrl, name)); } async function start() { const relayUrl = normalizeRelayUrl(relayInput.value); const name = normalizeName(nameInput.value); updateSharePreview(); if (!name) { setHint("Enter a broadcast name to watch.", "warn"); return; } if (!hasWebTransport()) { setHint( "WebTransport is not available in this browser. Try Chrome or Firefox Nightly. Safari support is still incomplete.", "warn", ); return; } try { await ensureHangWatchElement(); } catch (e) { setHint( `Failed to load web player dependency: ${String(e)}. Disable script blockers for esm.sh and retry.`, "warn", ); return; } writeParams(relayUrl, name); setHint(`Connecting to relay and subscribing: ${name}`, "ok"); mountPlayer(relayUrl, name); } relayInput.addEventListener("input", updateSharePreview); nameInput.addEventListener("input", updateSharePreview); watchBtn.addEventListener("click", () => { void start(); }); nameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") void start(); }); copyBtn.addEventListener("click", async () => { const relayUrl = normalizeRelayUrl(relayInput.value); const name = normalizeName(nameInput.value); if (!name) { setHint("Enter a broadcast name first.", "warn"); return; } const link = currentShareLink(relayUrl, name); try { await copyToClipboard(link); setHint("Link copied.", "ok"); setShareLink(link); } catch (e) { setHint(`Copy failed: ${String(e)}`, "warn"); setShareLink(link); } }); updateSharePreview(); // Auto-start if a name was provided. if (initial.name) void start(); } main();