ec-node: WebTransport publish + web hang-watch
This commit is contained in:
parent
791c7beee7
commit
339aef50e0
19 changed files with 1355 additions and 2229 deletions
181
apps/web/app.js
Normal file
181
apps/web/app.js
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
// every.channel web watcher
|
||||
//
|
||||
// This uses the upstream hang web component (WebTransport + WebCodecs).
|
||||
// It is intentionally dependency-light: no framework, no bundler.
|
||||
|
||||
import "https://cdn.jsdelivr.net/npm/@kixelated/hang@0.7.0/watch/element.js";
|
||||
|
||||
const DEFAULT_RELAY_URL = "https://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();
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue