// every.channel web watcher // // This uses the upstream hang web component (WebTransport + WebCodecs). // It is intentionally dependency-light: no framework, no bundler. // Use an ESM CDN that rewrites bare module specifiers. import "https://esm.sh/@kixelated/hang@0.7.4/watch/element.js"; const DEFAULT_RELAY_URL = "https://interop-relay.cloudflare.mediaoverquic.com/"; 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 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); const relay = u.searchParams.get("url"); const name = u.searchParams.get("name"); 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); 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)); } 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; } 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", start); nameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") 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) start(); } main();