web+publisher: align moq watch client and disable passthrough by default
This commit is contained in:
parent
5bce56ee79
commit
c2db3c6727
5 changed files with 88 additions and 23 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
This is a static web watcher site.
|
This is a static web watcher site.
|
||||||
|
|
||||||
It embeds the upstream `@kixelated/hang` WebTransport player component (`<hang-watch>`).
|
It embeds the upstream `@moq/watch` WebTransport player component (`<moq-watch>`).
|
||||||
|
|
||||||
## Dev
|
## Dev
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
// every.channel web watcher
|
// every.channel web watcher
|
||||||
//
|
//
|
||||||
// This uses the upstream hang web component (WebTransport + WebCodecs).
|
// This uses the upstream moq watch web component (WebTransport + WebCodecs).
|
||||||
// It is intentionally dependency-light: no framework, no bundler.
|
// It is intentionally dependency-light: no framework, no bundler.
|
||||||
|
|
||||||
const DEFAULT_RELAY_URL = "https://cdn.moq.dev/anon";
|
const DEFAULT_RELAY_URL = "https://cdn.moq.dev/anon";
|
||||||
const HANG_WATCH_MODULE_URL = "https://esm.sh/@kixelated/hang@0.7.0/watch/element.js";
|
const MOQ_WATCH_MODULE_URL = "https://esm.sh/@moq/watch@0.1.1/element";
|
||||||
let hangWatchModulePromise = null;
|
let moqWatchModulePromise = null;
|
||||||
|
let disposePlayerSignals = null;
|
||||||
|
|
||||||
function $(id) {
|
function $(id) {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
|
|
@ -47,14 +48,69 @@ function setShareLink(text) {
|
||||||
el.textContent = text || "";
|
el.textContent = text || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearPlayerSignals() {
|
||||||
|
if (typeof disposePlayerSignals === "function") {
|
||||||
|
disposePlayerSignals();
|
||||||
|
}
|
||||||
|
disposePlayerSignals = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindPlayerSignals(watch, name) {
|
||||||
|
const cleanup = [];
|
||||||
|
|
||||||
|
const maybeWatch = (signal, onValue) => {
|
||||||
|
if (!signal || typeof signal.watch !== "function") return;
|
||||||
|
const dispose = signal.watch(onValue);
|
||||||
|
if (typeof dispose === "function") cleanup.push(dispose);
|
||||||
|
};
|
||||||
|
|
||||||
|
let sawLoading = false;
|
||||||
|
maybeWatch(watch?.broadcast?.status, (status) => {
|
||||||
|
if (status === "loading") {
|
||||||
|
sawLoading = true;
|
||||||
|
setHint(`Connecting to relay and subscribing: ${name}`, "ok");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status === "live") {
|
||||||
|
setHint(`Live: subscribed to ${name}`, "ok");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status === "offline" && sawLoading) {
|
||||||
|
setHint(`Stream offline or ended: ${name}`, "warn");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
maybeWatch(watch?.broadcast?.catalog, (catalog) => {
|
||||||
|
if (!catalog) return;
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cleanup.length) {
|
||||||
|
disposePlayerSignals = () => {
|
||||||
|
for (const fn of cleanup) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} catch (_) {
|
||||||
|
// Best-effort cleanup only.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mountPlayer(relayUrl, name) {
|
function mountPlayer(relayUrl, name) {
|
||||||
|
clearPlayerSignals();
|
||||||
|
|
||||||
const mount = $("playerMount");
|
const mount = $("playerMount");
|
||||||
mount.textContent = "";
|
mount.textContent = "";
|
||||||
|
|
||||||
const watch = document.createElement("hang-watch");
|
const watch = document.createElement("moq-watch");
|
||||||
watch.setAttribute("url", relayUrl);
|
watch.setAttribute("url", relayUrl);
|
||||||
watch.setAttribute("name", name);
|
watch.setAttribute("path", name);
|
||||||
watch.setAttribute("controls", "");
|
|
||||||
|
|
||||||
// A canvas enables video rendering. Without it, only audio is played.
|
// A canvas enables video rendering. Without it, only audio is played.
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
|
|
@ -62,16 +118,17 @@ function mountPlayer(relayUrl, name) {
|
||||||
watch.appendChild(canvas);
|
watch.appendChild(canvas);
|
||||||
|
|
||||||
mount.appendChild(watch);
|
mount.appendChild(watch);
|
||||||
|
bindPlayerSignals(watch, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureHangWatchElement() {
|
async function ensureMoqWatchElement() {
|
||||||
if (window.customElements && window.customElements.get("hang-watch")) return;
|
if (window.customElements && window.customElements.get("moq-watch")) return;
|
||||||
if (!hangWatchModulePromise) {
|
if (!moqWatchModulePromise) {
|
||||||
hangWatchModulePromise = import(HANG_WATCH_MODULE_URL);
|
moqWatchModulePromise = import(MOQ_WATCH_MODULE_URL);
|
||||||
}
|
}
|
||||||
await hangWatchModulePromise;
|
await moqWatchModulePromise;
|
||||||
if (!(window.customElements && window.customElements.get("hang-watch"))) {
|
if (!(window.customElements && window.customElements.get("moq-watch"))) {
|
||||||
throw new Error("hang-watch custom element is unavailable");
|
throw new Error("moq-watch custom element is unavailable");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,10 +219,10 @@ function main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureHangWatchElement();
|
await ensureMoqWatchElement();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setHint(
|
setHint(
|
||||||
`Failed to load web player dependency: ${String(e)}. Disable script blockers for esm.sh and retry.`,
|
`Failed to load MoQ web player dependency: ${String(e)}. Disable script blockers for esm.sh and retry.`,
|
||||||
"warn",
|
"warn",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -401,8 +401,8 @@ struct WsSubscribeArgs {
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
struct WtPublishArgs {
|
struct WtPublishArgs {
|
||||||
/// Relay URL (WebTransport) to connect to.
|
/// Relay URL (WebTransport) to connect to.
|
||||||
/// Default points at Cloudflare's MoQ technical preview relay.
|
/// Default points at moq.dev's public relay.
|
||||||
#[arg(long, default_value = "https://relay.cloudflare.mediaoverquic.com/")]
|
#[arg(long, default_value = "https://cdn.moq.dev/anon")]
|
||||||
url: String,
|
url: String,
|
||||||
/// Broadcast name to publish.
|
/// Broadcast name to publish.
|
||||||
///
|
///
|
||||||
|
|
@ -418,7 +418,7 @@ struct WtPublishArgs {
|
||||||
transcode: bool,
|
transcode: bool,
|
||||||
/// Transmit fMP4 fragments directly (passthrough mode).
|
/// Transmit fMP4 fragments directly (passthrough mode).
|
||||||
/// When false, the importer may reframe into CMAF fragments.
|
/// When false, the importer may reframe into CMAF fragments.
|
||||||
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
|
#[arg(long, default_value_t = false, action = clap::ArgAction::Set)]
|
||||||
passthrough: bool,
|
passthrough: bool,
|
||||||
/// Danger: disable TLS verification for the relay.
|
/// Danger: disable TLS verification for the relay.
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ Adopt Cloudflare's MoQ relay preview as the default "global" distribution layer
|
||||||
Concrete changes:
|
Concrete changes:
|
||||||
|
|
||||||
1. `ec-node` gains a WebTransport MoQ publisher path that can publish a live CMAF (fMP4) stream to a relay URL (default: `https://cdn.moq.dev/anon`).
|
1. `ec-node` gains a WebTransport MoQ publisher path that can publish a live CMAF (fMP4) stream to a relay URL (default: `https://cdn.moq.dev/anon`).
|
||||||
2. `every.channel` (the deployed static site) becomes a real web watcher by embedding a WebTransport-capable MoQ player component (`<hang-watch>`).
|
2. `every.channel` (the deployed static site) becomes a real web watcher by embedding a WebTransport-capable MoQ player component (`<moq-watch>` from `@moq/watch`).
|
||||||
3. The existing WebRTC/WS bootstrap directory/relay remains temporarily for compatibility, but is treated as deprecated once MoQ/WebTransport is stable.
|
3. The existing WebRTC/WS bootstrap directory/relay remains temporarily for compatibility, but is treated as deprecated once MoQ/WebTransport is stable.
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
@ -48,11 +48,19 @@ As of February 21, 2026, browser sessions against `https://interop-relay.cloudfl
|
||||||
|
|
||||||
### Web player
|
### Web player
|
||||||
|
|
||||||
Use the `@kixelated/hang` web component:
|
Use the `@moq/watch` web component:
|
||||||
- `<hang-watch url="..." name="..." controls>`
|
- `<moq-watch url="..." path="...">`
|
||||||
|
|
||||||
This is WebTransport + WebCodecs based, and is expected to interoperate with Cloudflare's current relay preview.
|
This is WebTransport + WebCodecs based, and is expected to interoperate with Cloudflare's current relay preview.
|
||||||
|
|
||||||
|
### CMAF passthrough default
|
||||||
|
|
||||||
|
`ec-node wt-publish` defaults `--passthrough=false` (and Nix module default `passthrough = false`).
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Browser `@moq/watch` playback against live OTA ingest currently hits repeated `Invalid sample duration 0 in trun` decode failures when fed raw passthrough fMP4 fragments.
|
||||||
|
- Non-passthrough mode avoids this incompatibility and restores end-to-end playback reliability.
|
||||||
|
|
||||||
### Transport compatibility
|
### Transport compatibility
|
||||||
|
|
||||||
Cloudflare's public relay currently implements a subset of the IETF MoQ Transport draft-07 and may not interoperate with
|
Cloudflare's public relay currently implements a subset of the IETF MoQ Transport draft-07 and may not interoperate with
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ in
|
||||||
|
|
||||||
passthrough = lib.mkOption {
|
passthrough = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = true;
|
default = false;
|
||||||
description = "Whether to transmit fMP4 fragments directly (moq-mux passthrough).";
|
description = "Whether to transmit fMP4 fragments directly (moq-mux passthrough).";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue