@norskvideo/moq-watch 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"element.js","names":["#support","#signals","#render","#show","#details","#close","#getSummary","#renderHeader","#renderDetails"],"sources":["../../src/support/element.ts"],"sourcesContent":["/**\n * The `<moq-watch-support>` custom element: renders the {@link isSupported} report.\n *\n * Side-effectful: importing this registers the element.\n *\n * @module\n */\nimport { Effect, Signal } from \"@moq/signals\";\nimport * as DOM from \"@moq/signals/dom\";\nimport { type Codec, type Full, isSupported, type Level } from \"./\";\n\n// https://bugzilla.mozilla.org/show_bug.cgi?id=1967793\nconst isFirefox = navigator.userAgent.toLowerCase().includes(\"firefox\");\n\nconst OBSERVED = [\"show\", \"details\"] as const;\ntype Observed = (typeof OBSERVED)[number];\n\n// Whether to display the support banner.\n// - \"always\": Always display the banner.\n// - \"warning\": Display the banner if a required feature needs a polyfill/fallback.\n// - \"error\": Display the banner if a required feature is unsupported.\n// - \"never\": Never display the banner.\nexport type Show = \"always\" | \"warning\" | \"error\" | \"never\";\n\nexport default class MoqWatchSupport extends HTMLElement {\n\t#show = new Signal<Show>(\"warning\");\n\t#details = new Signal<boolean>(false);\n\t#support = new Signal<Full | undefined>(undefined);\n\t#close = new Signal<boolean>(false);\n\n\t#signals?: Effect;\n\n\tstatic observedAttributes = OBSERVED;\n\n\tconstructor() {\n\t\tsuper();\n\n\t\tisSupported()\n\t\t\t.then((s) => this.#support.set(s))\n\t\t\t.catch((err) => console.error(\"Failed to detect watch support:\", err));\n\t}\n\n\tconnectedCallback() {\n\t\tthis.#signals = new Effect();\n\t\tthis.#signals.run(this.#render.bind(this));\n\t}\n\n\tdisconnectedCallback() {\n\t\tthis.#signals?.close();\n\t\tthis.#signals = undefined;\n\t}\n\n\tattributeChangedCallback(name: Observed, _oldValue: string | null, newValue: string | null) {\n\t\tif (name === \"show\") {\n\t\t\tconst show = newValue ?? \"warning\";\n\t\t\tif (show === \"always\" || show === \"warning\" || show === \"error\" || show === \"never\") {\n\t\t\t\tthis.show = show;\n\t\t\t} else {\n\t\t\t\tthrow new Error(`Invalid show: ${show}`);\n\t\t\t}\n\t\t} else if (name === \"details\") {\n\t\t\tthis.details = newValue !== null;\n\t\t} else {\n\t\t\tconst exhaustive: never = name;\n\t\t\tthrow new Error(`Invalid attribute: ${exhaustive}`);\n\t\t}\n\t}\n\n\tget show(): Show {\n\t\treturn this.#show.peek();\n\t}\n\n\tset show(show: Show) {\n\t\tthis.#show.set(show);\n\t}\n\n\tget details(): boolean {\n\t\treturn this.#details.peek();\n\t}\n\n\tset details(details: boolean) {\n\t\tthis.#details.set(details);\n\t}\n\n\t#getSummary(support: Full): Level {\n\t\tif (support.webtransport === \"none\") return \"none\";\n\n\t\tif (!support.audio.decoding || !support.video.decoding) return \"none\";\n\t\tif (!support.audio.render || !support.video.render) return \"none\";\n\n\t\tif (!Object.values(support.audio.decoding).some((v) => v === true || v === \"full\" || v === \"partial\"))\n\t\t\treturn \"none\";\n\t\tif (!Object.values(support.video.decoding).some((v) => v.software || v.hardware)) return \"none\";\n\n\t\tif (!Object.values(support.audio.decoding).every((v) => v === true || v === \"full\")) return \"partial\";\n\t\tif (!Object.values(support.video.decoding).every((v) => v.software || v.hardware)) return \"partial\";\n\n\t\treturn \"full\";\n\t}\n\n\t#render(effect: Effect) {\n\t\tconst support = effect.get(this.#support);\n\t\tif (!support) return;\n\n\t\tconst close = effect.get(this.#close);\n\t\tif (close) return;\n\n\t\tconst show = effect.get(this.#show);\n\t\tif (show === \"never\") return;\n\n\t\tconst summary = this.#getSummary(support);\n\n\t\t// Don't render the banner if we have full support and they only asked for warnings.\n\t\tif (show === \"warning\" && summary === \"full\") return;\n\n\t\t// Don't render the banner if we have at least partial support and they only asked for errors.\n\t\tif (show === \"error\" && summary !== \"none\") return;\n\n\t\tconst container = DOM.create(\"div\", {\n\t\t\tstyle: {\n\t\t\t\tmargin: \"0 auto\",\n\t\t\t\tmaxWidth: \"28rem\",\n\t\t\t\tpadding: \"1rem\",\n\t\t\t},\n\t\t});\n\n\t\tthis.appendChild(container);\n\t\teffect.cleanup(() => this.removeChild(container));\n\n\t\tthis.#renderHeader(container, summary, effect);\n\n\t\tif (effect.get(this.#details)) {\n\t\t\tthis.#renderDetails(container, support, effect);\n\t\t}\n\t}\n\n\t#renderHeader(parent: HTMLDivElement, summary: Level, effect: Effect) {\n\t\tconst headerDiv = DOM.create(\"div\", {\n\t\t\tstyle: {\n\t\t\t\tdisplay: \"flex\",\n\t\t\t\tflexDirection: \"row\",\n\t\t\t\tgap: \"1rem\",\n\t\t\t\tflexWrap: \"wrap\",\n\t\t\t\tjustifyContent: \"space-between\",\n\t\t\t\talignItems: \"center\",\n\t\t\t},\n\t\t});\n\n\t\tconst statusDiv = DOM.create(\"div\", {\n\t\t\tstyle: { fontWeight: \"bold\" },\n\t\t});\n\n\t\tif (summary === \"full\") {\n\t\t\tstatusDiv.textContent = \"🟢 Full Browser Support\";\n\t\t} else if (summary === \"partial\") {\n\t\t\tstatusDiv.textContent = \"🟡 Partial Browser Support\";\n\t\t} else if (summary === \"none\") {\n\t\t\tstatusDiv.textContent = \"🔴 No Browser Support\";\n\t\t}\n\n\t\tconst detailsButton = DOM.create(\"button\", {\n\t\t\ttype: \"button\",\n\t\t\tstyle: { fontSize: \"14px\" },\n\t\t});\n\n\t\teffect.event(detailsButton, \"click\", () => {\n\t\t\tthis.#details.update((prev) => !prev);\n\t\t});\n\n\t\teffect.run((effect) => {\n\t\t\tdetailsButton.textContent = effect.get(this.#details) ? \"Details ➖\" : \"Details ➕\";\n\t\t});\n\n\t\tconst closeButton = DOM.create(\n\t\t\t\"button\",\n\t\t\t{\n\t\t\t\ttype: \"button\",\n\t\t\t\tstyle: { fontSize: \"14px\" },\n\t\t\t},\n\t\t\t\"Close ❌\",\n\t\t);\n\n\t\teffect.event(closeButton, \"click\", () => {\n\t\t\tthis.#close.set(true);\n\t\t});\n\n\t\theaderDiv.appendChild(statusDiv);\n\t\theaderDiv.appendChild(detailsButton);\n\t\theaderDiv.appendChild(closeButton);\n\n\t\tparent.appendChild(headerDiv);\n\t\teffect.cleanup(() => parent.removeChild(headerDiv));\n\t}\n\n\t#renderDetails(parent: HTMLDivElement, support: Full, effect: Effect) {\n\t\tconst container = DOM.create(\"div\", {\n\t\t\tstyle: {\n\t\t\t\tdisplay: \"grid\",\n\t\t\t\tgridTemplateColumns: \"1fr 1fr 1fr\",\n\t\t\t\tcolumnGap: \"0.5rem\",\n\t\t\t\trowGap: \"0.2rem\",\n\t\t\t\tbackgroundColor: \"rgba(0, 0, 0, 0.6)\",\n\t\t\t\tborderRadius: \"0.5rem\",\n\t\t\t\tpadding: \"1rem\",\n\t\t\t\tfontSize: \"0.875rem\",\n\t\t\t},\n\t\t});\n\n\t\tconst binary = (value: boolean | undefined) => (value ? \"🟢 Yes\" : \"🔴 No\");\n\t\tconst hardware = (codec: Codec | undefined) =>\n\t\t\tcodec?.hardware ? \"🟢 Hardware\" : codec?.software ? `🟡 Software${isFirefox ? \"*\" : \"\"}` : \"🔴 No\";\n\t\tconst partial = (value: Level | undefined) =>\n\t\t\tvalue === \"full\" ? \"🟢 Full\" : value === \"partial\" ? \"🟡 Polyfill\" : \"🔴 None\";\n\n\t\tconst addRow = (label: string, col2: string, col3: string) => {\n\t\t\tconst labelDiv = DOM.create(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tgridColumnStart: \"1\",\n\t\t\t\t\t\tfontWeight: \"bold\",\n\t\t\t\t\t\ttextAlign: \"right\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlabel,\n\t\t\t);\n\n\t\t\tconst col2Div = DOM.create(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tgridColumnStart: \"2\",\n\t\t\t\t\t\ttextAlign: \"center\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tcol2,\n\t\t\t);\n\n\t\t\tconst col3Div = DOM.create(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tstyle: { gridColumnStart: \"3\" },\n\t\t\t\t},\n\t\t\t\tcol3,\n\t\t\t);\n\n\t\t\tcontainer.appendChild(labelDiv);\n\t\t\tcontainer.appendChild(col2Div);\n\t\t\tcontainer.appendChild(col3Div);\n\t\t};\n\n\t\taddRow(\"WebTransport\", \"\", partial(support.webtransport));\n\t\taddRow(\"Rendering\", \"Audio\", binary(support.audio.render));\n\t\taddRow(\"\", \"Video\", binary(support.video.render));\n\t\taddRow(\"Decoding\", \"Opus\", partial(support.audio.decoding.opus));\n\t\taddRow(\"\", \"AAC\", binary(support.audio.decoding.aac));\n\t\taddRow(\"\", \"AV1\", hardware(support.video.decoding?.av1));\n\t\taddRow(\"\", \"H.265\", hardware(support.video.decoding?.h265));\n\t\taddRow(\"\", \"H.264\", hardware(support.video.decoding?.h264));\n\t\taddRow(\"\", \"VP9\", hardware(support.video.decoding?.vp9));\n\t\taddRow(\"\", \"VP8\", hardware(support.video.decoding?.vp8));\n\n\t\tif (isFirefox) {\n\t\t\tconst noteDiv = DOM.create(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tgridColumnStart: \"1\",\n\t\t\t\t\t\tgridColumnEnd: \"4\",\n\t\t\t\t\t\ttextAlign: \"center\",\n\t\t\t\t\t\tfontSize: \"0.875rem\",\n\t\t\t\t\t\tfontStyle: \"italic\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"Hardware acceleration is \",\n\t\t\t\tDOM.create(\n\t\t\t\t\t\"a\",\n\t\t\t\t\t{\n\t\t\t\t\t\thref: \"https://github.com/w3c/webcodecs/issues/896\",\n\t\t\t\t\t},\n\t\t\t\t\t\"undetectable\",\n\t\t\t\t),\n\t\t\t\t\" on Firefox.\",\n\t\t\t);\n\t\t\tcontainer.appendChild(noteDiv);\n\t\t}\n\n\t\tparent.appendChild(container);\n\t\teffect.cleanup(() => parent.removeChild(container));\n\t}\n}\n\ncustomElements.define(\"moq-watch-support\", MoqWatchSupport);\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t\"moq-watch-support\": MoqWatchSupport;\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"element.js","names":[],"sources":["../../src/support/element.ts"],"sourcesContent":["/**\n * The `<moq-watch-support>` custom element: renders the {@link isSupported} report.\n *\n * Side-effectful: importing this registers the element.\n *\n * @module\n */\nimport { Effect, Signal } from \"@moq/signals\";\nimport * as DOM from \"@moq/signals/dom\";\nimport { type Codec, type Full, isSupported, type Level } from \"./\";\n\n// https://bugzilla.mozilla.org/show_bug.cgi?id=1967793\nconst isFirefox = navigator.userAgent.toLowerCase().includes(\"firefox\");\n\nconst OBSERVED = [\"show\", \"details\"] as const;\ntype Observed = (typeof OBSERVED)[number];\n\n// Whether to display the support banner.\n// - \"always\": Always display the banner.\n// - \"warning\": Display the banner if a required feature needs a polyfill/fallback.\n// - \"error\": Display the banner if a required feature is unsupported.\n// - \"never\": Never display the banner.\nexport type Show = \"always\" | \"warning\" | \"error\" | \"never\";\n\nexport default class MoqWatchSupport extends HTMLElement {\n\t#show = new Signal<Show>(\"warning\");\n\t#details = new Signal<boolean>(false);\n\t#support = new Signal<Full | undefined>(undefined);\n\t#close = new Signal<boolean>(false);\n\n\t#signals?: Effect;\n\n\tstatic observedAttributes = OBSERVED;\n\n\tconstructor() {\n\t\tsuper();\n\n\t\tisSupported()\n\t\t\t.then((s) => this.#support.set(s))\n\t\t\t.catch((err) => console.error(\"Failed to detect watch support:\", err));\n\t}\n\n\tconnectedCallback() {\n\t\tthis.#signals = new Effect();\n\t\tthis.#signals.run(this.#render.bind(this));\n\t}\n\n\tdisconnectedCallback() {\n\t\tthis.#signals?.close();\n\t\tthis.#signals = undefined;\n\t}\n\n\tattributeChangedCallback(name: Observed, _oldValue: string | null, newValue: string | null) {\n\t\tif (name === \"show\") {\n\t\t\tconst show = newValue ?? \"warning\";\n\t\t\tif (show === \"always\" || show === \"warning\" || show === \"error\" || show === \"never\") {\n\t\t\t\tthis.show = show;\n\t\t\t} else {\n\t\t\t\tthrow new Error(`Invalid show: ${show}`);\n\t\t\t}\n\t\t} else if (name === \"details\") {\n\t\t\tthis.details = newValue !== null;\n\t\t} else {\n\t\t\tconst exhaustive: never = name;\n\t\t\tthrow new Error(`Invalid attribute: ${exhaustive}`);\n\t\t}\n\t}\n\n\tget show(): Show {\n\t\treturn this.#show.peek();\n\t}\n\n\tset show(show: Show) {\n\t\tthis.#show.set(show);\n\t}\n\n\tget details(): boolean {\n\t\treturn this.#details.peek();\n\t}\n\n\tset details(details: boolean) {\n\t\tthis.#details.set(details);\n\t}\n\n\t#getSummary(support: Full): Level {\n\t\tif (support.webtransport === \"none\") return \"none\";\n\n\t\tif (!support.audio.decoding || !support.video.decoding) return \"none\";\n\t\tif (!support.audio.render || !support.video.render) return \"none\";\n\n\t\tif (!Object.values(support.audio.decoding).some((v) => v === true || v === \"full\" || v === \"partial\"))\n\t\t\treturn \"none\";\n\t\tif (!Object.values(support.video.decoding).some((v) => v.software || v.hardware)) return \"none\";\n\n\t\tif (!Object.values(support.audio.decoding).every((v) => v === true || v === \"full\")) return \"partial\";\n\t\tif (!Object.values(support.video.decoding).every((v) => v.software || v.hardware)) return \"partial\";\n\n\t\treturn \"full\";\n\t}\n\n\t#render(effect: Effect) {\n\t\tconst support = effect.get(this.#support);\n\t\tif (!support) return;\n\n\t\tconst close = effect.get(this.#close);\n\t\tif (close) return;\n\n\t\tconst show = effect.get(this.#show);\n\t\tif (show === \"never\") return;\n\n\t\tconst summary = this.#getSummary(support);\n\n\t\t// Don't render the banner if we have full support and they only asked for warnings.\n\t\tif (show === \"warning\" && summary === \"full\") return;\n\n\t\t// Don't render the banner if we have at least partial support and they only asked for errors.\n\t\tif (show === \"error\" && summary !== \"none\") return;\n\n\t\tconst container = DOM.create(\"div\", {\n\t\t\tstyle: {\n\t\t\t\tmargin: \"0 auto\",\n\t\t\t\tmaxWidth: \"28rem\",\n\t\t\t\tpadding: \"1rem\",\n\t\t\t},\n\t\t});\n\n\t\tthis.appendChild(container);\n\t\teffect.cleanup(() => this.removeChild(container));\n\n\t\tthis.#renderHeader(container, summary, effect);\n\n\t\tif (effect.get(this.#details)) {\n\t\t\tthis.#renderDetails(container, support, effect);\n\t\t}\n\t}\n\n\t#renderHeader(parent: HTMLDivElement, summary: Level, effect: Effect) {\n\t\tconst headerDiv = DOM.create(\"div\", {\n\t\t\tstyle: {\n\t\t\t\tdisplay: \"flex\",\n\t\t\t\tflexDirection: \"row\",\n\t\t\t\tgap: \"1rem\",\n\t\t\t\tflexWrap: \"wrap\",\n\t\t\t\tjustifyContent: \"space-between\",\n\t\t\t\talignItems: \"center\",\n\t\t\t},\n\t\t});\n\n\t\tconst statusDiv = DOM.create(\"div\", {\n\t\t\tstyle: { fontWeight: \"bold\" },\n\t\t});\n\n\t\tif (summary === \"full\") {\n\t\t\tstatusDiv.textContent = \"🟢 Full Browser Support\";\n\t\t} else if (summary === \"partial\") {\n\t\t\tstatusDiv.textContent = \"🟡 Partial Browser Support\";\n\t\t} else if (summary === \"none\") {\n\t\t\tstatusDiv.textContent = \"🔴 No Browser Support\";\n\t\t}\n\n\t\tconst detailsButton = DOM.create(\"button\", {\n\t\t\ttype: \"button\",\n\t\t\tstyle: { fontSize: \"14px\" },\n\t\t});\n\n\t\teffect.event(detailsButton, \"click\", () => {\n\t\t\tthis.#details.update((prev) => !prev);\n\t\t});\n\n\t\teffect.run((effect) => {\n\t\t\tdetailsButton.textContent = effect.get(this.#details) ? \"Details ➖\" : \"Details ➕\";\n\t\t});\n\n\t\tconst closeButton = DOM.create(\n\t\t\t\"button\",\n\t\t\t{\n\t\t\t\ttype: \"button\",\n\t\t\t\tstyle: { fontSize: \"14px\" },\n\t\t\t},\n\t\t\t\"Close ❌\",\n\t\t);\n\n\t\teffect.event(closeButton, \"click\", () => {\n\t\t\tthis.#close.set(true);\n\t\t});\n\n\t\theaderDiv.appendChild(statusDiv);\n\t\theaderDiv.appendChild(detailsButton);\n\t\theaderDiv.appendChild(closeButton);\n\n\t\tparent.appendChild(headerDiv);\n\t\teffect.cleanup(() => parent.removeChild(headerDiv));\n\t}\n\n\t#renderDetails(parent: HTMLDivElement, support: Full, effect: Effect) {\n\t\tconst container = DOM.create(\"div\", {\n\t\t\tstyle: {\n\t\t\t\tdisplay: \"grid\",\n\t\t\t\tgridTemplateColumns: \"1fr 1fr 1fr\",\n\t\t\t\tcolumnGap: \"0.5rem\",\n\t\t\t\trowGap: \"0.2rem\",\n\t\t\t\tbackgroundColor: \"rgba(0, 0, 0, 0.6)\",\n\t\t\t\tborderRadius: \"0.5rem\",\n\t\t\t\tpadding: \"1rem\",\n\t\t\t\tfontSize: \"0.875rem\",\n\t\t\t},\n\t\t});\n\n\t\tconst binary = (value: boolean | undefined) => (value ? \"🟢 Yes\" : \"🔴 No\");\n\t\tconst hardware = (codec: Codec | undefined) =>\n\t\t\tcodec?.hardware ? \"🟢 Hardware\" : codec?.software ? `🟡 Software${isFirefox ? \"*\" : \"\"}` : \"🔴 No\";\n\t\tconst partial = (value: Level | undefined) =>\n\t\t\tvalue === \"full\" ? \"🟢 Full\" : value === \"partial\" ? \"🟡 Polyfill\" : \"🔴 None\";\n\n\t\tconst addRow = (label: string, col2: string, col3: string) => {\n\t\t\tconst labelDiv = DOM.create(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tgridColumnStart: \"1\",\n\t\t\t\t\t\tfontWeight: \"bold\",\n\t\t\t\t\t\ttextAlign: \"right\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlabel,\n\t\t\t);\n\n\t\t\tconst col2Div = DOM.create(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tgridColumnStart: \"2\",\n\t\t\t\t\t\ttextAlign: \"center\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tcol2,\n\t\t\t);\n\n\t\t\tconst col3Div = DOM.create(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tstyle: { gridColumnStart: \"3\" },\n\t\t\t\t},\n\t\t\t\tcol3,\n\t\t\t);\n\n\t\t\tcontainer.appendChild(labelDiv);\n\t\t\tcontainer.appendChild(col2Div);\n\t\t\tcontainer.appendChild(col3Div);\n\t\t};\n\n\t\taddRow(\"WebTransport\", \"\", partial(support.webtransport));\n\t\taddRow(\"Rendering\", \"Audio\", binary(support.audio.render));\n\t\taddRow(\"\", \"Video\", binary(support.video.render));\n\t\taddRow(\"Decoding\", \"Opus\", partial(support.audio.decoding.opus));\n\t\taddRow(\"\", \"AAC\", binary(support.audio.decoding.aac));\n\t\taddRow(\"\", \"AV1\", hardware(support.video.decoding?.av1));\n\t\taddRow(\"\", \"H.265\", hardware(support.video.decoding?.h265));\n\t\taddRow(\"\", \"H.264\", hardware(support.video.decoding?.h264));\n\t\taddRow(\"\", \"VP9\", hardware(support.video.decoding?.vp9));\n\t\taddRow(\"\", \"VP8\", hardware(support.video.decoding?.vp8));\n\n\t\tif (isFirefox) {\n\t\t\tconst noteDiv = DOM.create(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tgridColumnStart: \"1\",\n\t\t\t\t\t\tgridColumnEnd: \"4\",\n\t\t\t\t\t\ttextAlign: \"center\",\n\t\t\t\t\t\tfontSize: \"0.875rem\",\n\t\t\t\t\t\tfontStyle: \"italic\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"Hardware acceleration is \",\n\t\t\t\tDOM.create(\n\t\t\t\t\t\"a\",\n\t\t\t\t\t{\n\t\t\t\t\t\thref: \"https://github.com/w3c/webcodecs/issues/896\",\n\t\t\t\t\t},\n\t\t\t\t\t\"undetectable\",\n\t\t\t\t),\n\t\t\t\t\" on Firefox.\",\n\t\t\t);\n\t\t\tcontainer.appendChild(noteDiv);\n\t\t}\n\n\t\tparent.appendChild(container);\n\t\teffect.cleanup(() => parent.removeChild(container));\n\t}\n}\n\ncustomElements.define(\"moq-watch-support\", MoqWatchSupport);\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t\"moq-watch-support\": MoqWatchSupport;\n\t}\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sync-DNQkgnlX.js","names":["#out","#update","#signals","#runJitter","#runBuffer","#runRange","#minRtt","#setReference","#late","#formatDuration","#latencyCap"],"sources":["../src/sync.ts"],"sourcesContent":["import type * as Moq from \"@moq/net\";\nimport { Time } from \"@moq/net\";\nimport { Effect, type Getter, getter, type Inputs, type Readonlys, readonlys, Signal } from \"@moq/signals\";\n\n/** A single latency bound: `\"real-time\"` adapts to the RTT; a `Time.Milli` fixes the jitter buffer. */\nexport type Bound = \"real-time\" | Time.Milli;\n\n/**\n * Latency target. A scalar (or `\"real-time\"`) collapses the range and minimizes latency, the live\n * default. An object opens a range `[min, max]`: playback buffers freely between the floor and the\n * ceiling and only skips ahead once latency would exceed the ceiling, so faster-than-real-time\n * frames (e.g. a TTS response with future timestamps) build up instead of being skipped. Both\n * bounds default to `\"real-time\"` when omitted. The ceiling is always finite (no uncapped buffering),\n * so worst case the audio ring drops its oldest samples rather than exhausting memory.\n *\n * `\"instant\"` drops the clock instead of bounding it: video paints the moment it decodes and\n * audio is disabled. It replaces the range rather than sitting inside it, so it is not a\n * {@link Bound} and cannot be used as a floor or ceiling. Not pacing video and disabling audio is\n * the owner's job, so a {@link Sync} handed this value resolves it to a zero buffer and no more.\n */\nexport type Latency = \"instant\" | Paced;\n\n/** A latency target the playback clock can pace off on its own: every {@link Latency} but `\"instant\"`. */\nexport type Paced = Bound | { min?: Bound; max?: Bound };\n\n/**\n * Resolve a {@link Latency} into explicit floor/ceiling bounds (a scalar collapses to `min == max`).\n * `\"instant\"` holds nothing, so both bounds are zero.\n */\nexport function latencyBounds(latency: Latency): { min: Bound; max: Bound } {\n\tif (latency === \"instant\") return { min: Time.Milli.zero, max: Time.Milli.zero };\n\tif (latency === \"real-time\" || typeof latency === \"number\") {\n\t\treturn { min: latency, max: latency };\n\t}\n\treturn { min: latency.min ?? \"real-time\", max: latency.max ?? \"real-time\" };\n}\n\n/** Build a {@link Paced} latency from explicit bounds, collapsing to a scalar when they're equal. */\nexport function latencyFromBounds(min: Bound, max: Bound): Paced {\n\treturn min === max ? min : { min, max };\n}\n\nconst MIN_JITTER = Time.Milli(20);\nconst FALLBACK_JITTER = Time.Milli(100);\n\nexport type SyncInput = {\n\t/**\n\t * Latency target: a scalar minimizes (collapsed range), an object opens a range. See {@link Paced}.\n\t *\n\t * A clock cannot implement `\"instant\"` on its own, since not pacing video and disabling audio\n\t * are the owner's job. Pass a {@link Paced} value: `\"instant\"` resolves to a zero buffer here.\n\t */\n\tlatency: Getter<Latency>;\n\n\t/** The connection used for \"real-time\" jitter: PROBE supplies RTT. */\n\tconnection: Getter<Moq.Connection.Established | undefined>;\n\n\t/** Any additional delay required for audio (wired from the per-rendition source). */\n\taudio: Getter<Time.Milli | undefined>;\n\n\t/** Any additional delay required for video (wired from the per-rendition source). */\n\tvideo: Getter<Time.Milli | undefined>;\n};\n\ntype SyncOutput = {\n\t// The earliest time we've received a frame, relative to its timestamp.\n\t// This will keep being updated as we catch up to the live playhead then will be relatively static.\n\treference: Signal<Time.Milli | undefined>;\n\n\t// The total buffer required: jitter + max(audio, video).\n\tbuffer: Signal<Time.Milli>;\n\n\t// The jitter buffer in milliseconds (always numeric).\n\t// In \"real-time\" mode this is updated automatically from RTT.\n\t// When the floor is a number, jitter equals that number.\n\tjitter: Signal<Time.Milli>;\n\n\t// The media timestamp of the most recently received frame.\n\ttimestamp: Signal<Time.Milli | undefined>;\n\n\t// Derived: true when the ceiling sits above the floor. Buffered playback lets the reference\n\t// stay anchored so future-dated frames build up a buffer, re-anchoring (skipping ahead) only\n\t// when latency would exceed the ceiling. See `reset()`.\n\tbuffered: Signal<boolean>;\n\n\t// Derived cap on buffered audio (ms), consumed by the audio ring to size itself. Always finite.\n\tmaxBuffer: Signal<Time.Milli>;\n};\n\nexport class Sync {\n\treadonly in: Readonlys<SyncInput>;\n\n\treadonly #out: SyncOutput = {\n\t\treference: new Signal<Time.Milli | undefined>(undefined),\n\t\tbuffer: new Signal<Time.Milli>(Time.Milli.zero),\n\t\tjitter: new Signal<Time.Milli>(FALLBACK_JITTER),\n\t\ttimestamp: new Signal<Time.Milli | undefined>(undefined),\n\t\tbuffered: new Signal<boolean>(false),\n\t\tmaxBuffer: new Signal<Time.Milli>(Time.Milli.zero),\n\t};\n\treadonly out = readonlys(this.#out);\n\n\t// A ghetto way to learn when the reference/buffer changes.\n\t// There's probably a way to use Effect, but lets keep it simple for now.\n\t#update: PromiseWithResolvers<void>;\n\n\t// Per-label late-frame tracking: accumulate count and max lateness, flush on recovery.\n\t#late = new Map<string, { count: number; maxMs: number }>();\n\n\t// Minimum RTT seen, used as the baseline for jitter calculation.\n\t// Avoids inflating jitter due to bufferbloat.\n\t#minRtt: number | undefined;\n\n\t#signals = new Effect();\n\n\tconstructor(props?: Inputs<SyncInput>) {\n\t\tthis.in = {\n\t\t\tlatency: getter(props?.latency ?? (\"real-time\" as Latency)),\n\t\t\tconnection: getter(props?.connection),\n\t\t\taudio: getter(props?.audio),\n\t\t\tvideo: getter(props?.video),\n\t\t};\n\n\t\tthis.#update = Promise.withResolvers();\n\n\t\tthis.#signals.run(this.#runJitter.bind(this));\n\t\tthis.#signals.run(this.#runBuffer.bind(this));\n\t\tthis.#signals.run(this.#runRange.bind(this));\n\t}\n\n\t// Derive `buffered` / `maxBuffer` from the floor (`buffer`) and the ceiling (the `max` bound).\n\t#runRange(effect: Effect): void {\n\t\tconst { max } = latencyBounds(effect.get(this.in.latency));\n\t\tconst floor = effect.get(this.#out.buffer);\n\n\t\tif (max === \"real-time\") {\n\t\t\t// Ceiling tracks the floor: minimize latency, the live default.\n\t\t\tthis.#out.buffered.set(false);\n\t\t\tthis.#out.maxBuffer.set(floor);\n\t\t} else {\n\t\t\t// Buffered only when the ceiling is above the floor; otherwise it collapses to minimize.\n\t\t\tthis.#out.buffered.set(max > floor);\n\t\t\tthis.#out.maxBuffer.set(Time.Milli.max(max, floor));\n\t\t}\n\t}\n\n\t// The maximum total latency (lookahead + floor) we tolerate before re-anchoring, in ms.\n\t// Used by `received()` to decide when to skip ahead.\n\t#latencyCap(): Time.Milli {\n\t\tconst { max } = latencyBounds(this.in.latency.peek());\n\t\tconst floor = this.#out.buffer.peek();\n\t\tif (max === \"real-time\") return floor;\n\t\treturn Time.Milli.max(max, floor);\n\t}\n\n\t#runJitter(effect: Effect): void {\n\t\tconst { min } = latencyBounds(effect.get(this.in.latency));\n\n\t\tif (typeof min === \"number\") {\n\t\t\t// Fixed mode: the floor value is the jitter.\n\t\t\tthis.#minRtt = undefined;\n\t\t\tthis.#out.jitter.set(min);\n\t\t\treturn;\n\t\t}\n\n\t\t// \"real-time\" mode: compute jitter from RTT on the established connection.\n\t\tconst conn = effect.get(this.in.connection);\n\t\tconst rtt = conn && effect.get(conn.probe).rtt;\n\t\tif (rtt !== undefined) {\n\t\t\t// Track minimum RTT as baseline, ignoring bufferbloat.\n\t\t\tthis.#minRtt = this.#minRtt !== undefined ? Math.min(this.#minRtt, rtt) : rtt;\n\n\t\t\t// Buffer enough for a retransmit (1 RTT for ACK + retransmit).\n\t\t\tconst jitter = Time.Milli(Math.max(MIN_JITTER, this.#minRtt * 1.25));\n\t\t\tthis.#out.jitter.set(jitter);\n\t\t\treturn;\n\t\t}\n\n\t\t// No RTT available: fall back to static default.\n\t\tthis.#minRtt = undefined;\n\t\tthis.#out.jitter.set(FALLBACK_JITTER);\n\t}\n\n\t#runBuffer(effect: Effect): void {\n\t\tconst jitter = effect.get(this.#out.jitter);\n\t\tconst video = effect.get(this.in.video) ?? Time.Milli.zero;\n\t\tconst audio = effect.get(this.in.audio) ?? Time.Milli.zero;\n\n\t\tconst buffer = Time.Milli.add(Time.Milli.max(video, audio), jitter);\n\t\tthis.#out.buffer.set(buffer);\n\n\t\tthis.#update.resolve();\n\t\tthis.#update = Promise.withResolvers();\n\t}\n\n\t// Fold a newly received frame into the reference. The reference anchors playback to the\n\t// wall clock; we lower it (skip ahead) only when keeping it would push latency past the cap.\n\treceived(timestamp: Time.Milli, label = \"\"): void {\n\t\tthis.#out.timestamp.update((current) => (current === undefined || timestamp > current ? timestamp : current));\n\t\tconst now = Time.Milli.now();\n\t\tconst ref = Time.Milli.sub(now, timestamp);\n\t\tconst currentRef = this.#out.reference.peek();\n\n\t\t// First frame anchors the reference.\n\t\tif (currentRef === undefined) {\n\t\t\tthis.#setReference(ref);\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if `wait()` would not sleep at all.\n\t\t// NOTE: We check here instead of in `wait()` so we can identify when frames are received late.\n\t\t// Otherwise, chained `wait()` calls would cause a false-positive during CPU starvation.\n\t\tconst floor = this.#out.buffer.peek();\n\t\tconst sleep = Time.Milli.add(Time.Milli.sub(currentRef, ref), floor);\n\t\tif (sleep < 0) {\n\t\t\tconst entry = this.#late.get(label);\n\t\t\tif (entry) {\n\t\t\t\tentry.count++;\n\t\t\t\tentry.maxMs = Math.max(entry.maxMs, -sleep);\n\t\t\t} else {\n\t\t\t\tthis.#late.set(label, { count: 1, maxMs: -sleep });\n\t\t\t}\n\t\t} else {\n\t\t\tconst entry = this.#late.get(label);\n\t\t\tif (entry) {\n\t\t\t\tconst prefix = label ? `sync[${label}]` : \"sync\";\n\t\t\t\tconst behind = Sync.#formatDuration(entry.maxMs);\n\t\t\t\tconsole.debug(`${prefix}: ${entry.count} late frame(s), max ${behind} behind`);\n\t\t\t\tthis.#late.delete(label);\n\t\t\t}\n\t\t}\n\n\t\t// Frame isn't earlier than the anchor: it can't lower latency, so keep the reference.\n\t\tif (ref >= currentRef) return;\n\n\t\t// Frame is earlier (more lookahead). `sleep` is the latency keeping the anchor would impose.\n\t\tconst cap = this.#latencyCap();\n\t\tif (sleep <= cap) return; // within budget: let the buffer grow instead of skipping ahead\n\n\t\t// Over the cap: re-anchor down so the resulting latency is exactly the cap.\n\t\tthis.#setReference(Time.Milli.add(ref, Time.Milli.sub(cap, floor)));\n\t}\n\n\t#setReference(ref: Time.Milli): void {\n\t\tthis.#out.reference.set(ref);\n\t\tthis.#update.resolve();\n\t\tthis.#update = Promise.withResolvers();\n\t}\n\n\t// Re-anchor playback to the next frame received. Call this at an utterance boundary\n\t// in buffered mode (typically alongside flushing the audio buffer) so the new content\n\t// plays from its own first frame instead of inheriting the previous reference.\n\treset(): void {\n\t\tthis.#out.reference.set(undefined);\n\t\tthis.#late.clear();\n\t\tthis.#update.resolve();\n\t\tthis.#update = Promise.withResolvers();\n\t}\n\n\t// The PTS that should be rendering right now, derived from the reference + buffer.\n\t// Returns undefined if no frames have been received yet.\n\tnow(): Time.Milli | undefined {\n\t\tconst reference = this.#out.reference.peek();\n\t\tif (reference === undefined) return undefined;\n\t\treturn Time.Milli.sub(Time.Milli.sub(Time.Milli.now(), reference), this.#out.buffer.peek());\n\t}\n\n\t// Sleep until it's time to render this frame.\n\tasync wait(timestamp: Time.Milli): Promise<void> {\n\t\tconst reference = this.#out.reference.peek();\n\t\tif (reference === undefined) {\n\t\t\tthrow new Error(\"reference not set; call received() first\");\n\t\t}\n\n\t\tfor (;;) {\n\t\t\t// Sleep until it's time to decode the next frame.\n\t\t\t// NOTE: This function runs in parallel for each frame.\n\t\t\tconst now = Time.Milli.now();\n\t\t\tconst ref = Time.Milli.sub(now, timestamp);\n\n\t\t\tconst currentRef = this.#out.reference.peek();\n\t\t\tif (currentRef === undefined) return;\n\n\t\t\tconst sleep = Time.Milli.add(Time.Milli.sub(currentRef, ref), this.#out.buffer.peek());\n\t\t\tif (sleep <= 0) return;\n\n\t\t\t// Skip setTimeout for small sleeps; the timer resolution (~4ms) would overshoot.\n\t\t\tif (sleep < 5) return;\n\n\t\t\tconst wait = new Promise((resolve) => setTimeout(resolve, sleep)).then(() => true);\n\n\t\t\tconst ok = await Promise.race([this.#update.promise, wait]);\n\t\t\tif (ok) return;\n\t\t}\n\t}\n\n\tstatic #formatDuration(ms: number): string {\n\t\tms = Math.round(ms);\n\t\tif (ms < 1000) return `${ms}ms`;\n\t\tconst s = ms / 1000;\n\t\tif (s < 60) return `${Math.round(s * 10) / 10}s`;\n\t\tconst m = s / 60;\n\t\treturn `${Math.round(m * 10) / 10}m`;\n\t}\n\n\tclose() {\n\t\tthis.#signals.close();\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sync-DNQkgnlX.js","names":[],"sources":["../src/sync.ts"],"sourcesContent":["import type * as Moq from \"@moq/net\";\nimport { Time } from \"@moq/net\";\nimport { Effect, type Getter, getter, type Inputs, type Readonlys, readonlys, Signal } from \"@moq/signals\";\n\n/** A single latency bound: `\"real-time\"` adapts to the RTT; a `Time.Milli` fixes the jitter buffer. */\nexport type Bound = \"real-time\" | Time.Milli;\n\n/**\n * Latency target. A scalar (or `\"real-time\"`) collapses the range and minimizes latency, the live\n * default. An object opens a range `[min, max]`: playback buffers freely between the floor and the\n * ceiling and only skips ahead once latency would exceed the ceiling, so faster-than-real-time\n * frames (e.g. a TTS response with future timestamps) build up instead of being skipped. Both\n * bounds default to `\"real-time\"` when omitted. The ceiling is always finite (no uncapped buffering),\n * so worst case the audio ring drops its oldest samples rather than exhausting memory.\n *\n * `\"instant\"` drops the clock instead of bounding it: video paints the moment it decodes and\n * audio is disabled. It replaces the range rather than sitting inside it, so it is not a\n * {@link Bound} and cannot be used as a floor or ceiling. Not pacing video and disabling audio is\n * the owner's job, so a {@link Sync} handed this value resolves it to a zero buffer and no more.\n */\nexport type Latency = \"instant\" | Paced;\n\n/** A latency target the playback clock can pace off on its own: every {@link Latency} but `\"instant\"`. */\nexport type Paced = Bound | { min?: Bound; max?: Bound };\n\n/**\n * Resolve a {@link Latency} into explicit floor/ceiling bounds (a scalar collapses to `min == max`).\n * `\"instant\"` holds nothing, so both bounds are zero.\n */\nexport function latencyBounds(latency: Latency): { min: Bound; max: Bound } {\n\tif (latency === \"instant\") return { min: Time.Milli.zero, max: Time.Milli.zero };\n\tif (latency === \"real-time\" || typeof latency === \"number\") {\n\t\treturn { min: latency, max: latency };\n\t}\n\treturn { min: latency.min ?? \"real-time\", max: latency.max ?? \"real-time\" };\n}\n\n/** Build a {@link Paced} latency from explicit bounds, collapsing to a scalar when they're equal. */\nexport function latencyFromBounds(min: Bound, max: Bound): Paced {\n\treturn min === max ? min : { min, max };\n}\n\nconst MIN_JITTER = Time.Milli(20);\nconst FALLBACK_JITTER = Time.Milli(100);\n\nexport type SyncInput = {\n\t/**\n\t * Latency target: a scalar minimizes (collapsed range), an object opens a range. See {@link Paced}.\n\t *\n\t * A clock cannot implement `\"instant\"` on its own, since not pacing video and disabling audio\n\t * are the owner's job. Pass a {@link Paced} value: `\"instant\"` resolves to a zero buffer here.\n\t */\n\tlatency: Getter<Latency>;\n\n\t/** The connection used for \"real-time\" jitter: PROBE supplies RTT. */\n\tconnection: Getter<Moq.Connection.Established | undefined>;\n\n\t/** Any additional delay required for audio (wired from the per-rendition source). */\n\taudio: Getter<Time.Milli | undefined>;\n\n\t/** Any additional delay required for video (wired from the per-rendition source). */\n\tvideo: Getter<Time.Milli | undefined>;\n};\n\ntype SyncOutput = {\n\t// The earliest time we've received a frame, relative to its timestamp.\n\t// This will keep being updated as we catch up to the live playhead then will be relatively static.\n\treference: Signal<Time.Milli | undefined>;\n\n\t// The total buffer required: jitter + max(audio, video).\n\tbuffer: Signal<Time.Milli>;\n\n\t// The jitter buffer in milliseconds (always numeric).\n\t// In \"real-time\" mode this is updated automatically from RTT.\n\t// When the floor is a number, jitter equals that number.\n\tjitter: Signal<Time.Milli>;\n\n\t// The media timestamp of the most recently received frame.\n\ttimestamp: Signal<Time.Milli | undefined>;\n\n\t// Derived: true when the ceiling sits above the floor. Buffered playback lets the reference\n\t// stay anchored so future-dated frames build up a buffer, re-anchoring (skipping ahead) only\n\t// when latency would exceed the ceiling. See `reset()`.\n\tbuffered: Signal<boolean>;\n\n\t// Derived cap on buffered audio (ms), consumed by the audio ring to size itself. Always finite.\n\tmaxBuffer: Signal<Time.Milli>;\n};\n\nexport class Sync {\n\treadonly in: Readonlys<SyncInput>;\n\n\treadonly #out: SyncOutput = {\n\t\treference: new Signal<Time.Milli | undefined>(undefined),\n\t\tbuffer: new Signal<Time.Milli>(Time.Milli.zero),\n\t\tjitter: new Signal<Time.Milli>(FALLBACK_JITTER),\n\t\ttimestamp: new Signal<Time.Milli | undefined>(undefined),\n\t\tbuffered: new Signal<boolean>(false),\n\t\tmaxBuffer: new Signal<Time.Milli>(Time.Milli.zero),\n\t};\n\treadonly out = readonlys(this.#out);\n\n\t// A ghetto way to learn when the reference/buffer changes.\n\t// There's probably a way to use Effect, but lets keep it simple for now.\n\t#update: PromiseWithResolvers<void>;\n\n\t// Per-label late-frame tracking: accumulate count and max lateness, flush on recovery.\n\t#late = new Map<string, { count: number; maxMs: number }>();\n\n\t// Minimum RTT seen, used as the baseline for jitter calculation.\n\t// Avoids inflating jitter due to bufferbloat.\n\t#minRtt: number | undefined;\n\n\t#signals = new Effect();\n\n\tconstructor(props?: Inputs<SyncInput>) {\n\t\tthis.in = {\n\t\t\tlatency: getter(props?.latency ?? (\"real-time\" as Latency)),\n\t\t\tconnection: getter(props?.connection),\n\t\t\taudio: getter(props?.audio),\n\t\t\tvideo: getter(props?.video),\n\t\t};\n\n\t\tthis.#update = Promise.withResolvers();\n\n\t\tthis.#signals.run(this.#runJitter.bind(this));\n\t\tthis.#signals.run(this.#runBuffer.bind(this));\n\t\tthis.#signals.run(this.#runRange.bind(this));\n\t}\n\n\t// Derive `buffered` / `maxBuffer` from the floor (`buffer`) and the ceiling (the `max` bound).\n\t#runRange(effect: Effect): void {\n\t\tconst { max } = latencyBounds(effect.get(this.in.latency));\n\t\tconst floor = effect.get(this.#out.buffer);\n\n\t\tif (max === \"real-time\") {\n\t\t\t// Ceiling tracks the floor: minimize latency, the live default.\n\t\t\tthis.#out.buffered.set(false);\n\t\t\tthis.#out.maxBuffer.set(floor);\n\t\t} else {\n\t\t\t// Buffered only when the ceiling is above the floor; otherwise it collapses to minimize.\n\t\t\tthis.#out.buffered.set(max > floor);\n\t\t\tthis.#out.maxBuffer.set(Time.Milli.max(max, floor));\n\t\t}\n\t}\n\n\t// The maximum total latency (lookahead + floor) we tolerate before re-anchoring, in ms.\n\t// Used by `received()` to decide when to skip ahead.\n\t#latencyCap(): Time.Milli {\n\t\tconst { max } = latencyBounds(this.in.latency.peek());\n\t\tconst floor = this.#out.buffer.peek();\n\t\tif (max === \"real-time\") return floor;\n\t\treturn Time.Milli.max(max, floor);\n\t}\n\n\t#runJitter(effect: Effect): void {\n\t\tconst { min } = latencyBounds(effect.get(this.in.latency));\n\n\t\tif (typeof min === \"number\") {\n\t\t\t// Fixed mode: the floor value is the jitter.\n\t\t\tthis.#minRtt = undefined;\n\t\t\tthis.#out.jitter.set(min);\n\t\t\treturn;\n\t\t}\n\n\t\t// \"real-time\" mode: compute jitter from RTT on the established connection.\n\t\tconst conn = effect.get(this.in.connection);\n\t\tconst rtt = conn && effect.get(conn.probe).rtt;\n\t\tif (rtt !== undefined) {\n\t\t\t// Track minimum RTT as baseline, ignoring bufferbloat.\n\t\t\tthis.#minRtt = this.#minRtt !== undefined ? Math.min(this.#minRtt, rtt) : rtt;\n\n\t\t\t// Buffer enough for a retransmit (1 RTT for ACK + retransmit).\n\t\t\tconst jitter = Time.Milli(Math.max(MIN_JITTER, this.#minRtt * 1.25));\n\t\t\tthis.#out.jitter.set(jitter);\n\t\t\treturn;\n\t\t}\n\n\t\t// No RTT available: fall back to static default.\n\t\tthis.#minRtt = undefined;\n\t\tthis.#out.jitter.set(FALLBACK_JITTER);\n\t}\n\n\t#runBuffer(effect: Effect): void {\n\t\tconst jitter = effect.get(this.#out.jitter);\n\t\tconst video = effect.get(this.in.video) ?? Time.Milli.zero;\n\t\tconst audio = effect.get(this.in.audio) ?? Time.Milli.zero;\n\n\t\tconst buffer = Time.Milli.add(Time.Milli.max(video, audio), jitter);\n\t\tthis.#out.buffer.set(buffer);\n\n\t\tthis.#update.resolve();\n\t\tthis.#update = Promise.withResolvers();\n\t}\n\n\t// Fold a newly received frame into the reference. The reference anchors playback to the\n\t// wall clock; we lower it (skip ahead) only when keeping it would push latency past the cap.\n\treceived(timestamp: Time.Milli, label = \"\"): void {\n\t\tthis.#out.timestamp.update((current) => (current === undefined || timestamp > current ? timestamp : current));\n\t\tconst now = Time.Milli.now();\n\t\tconst ref = Time.Milli.sub(now, timestamp);\n\t\tconst currentRef = this.#out.reference.peek();\n\n\t\t// First frame anchors the reference.\n\t\tif (currentRef === undefined) {\n\t\t\tthis.#setReference(ref);\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if `wait()` would not sleep at all.\n\t\t// NOTE: We check here instead of in `wait()` so we can identify when frames are received late.\n\t\t// Otherwise, chained `wait()` calls would cause a false-positive during CPU starvation.\n\t\tconst floor = this.#out.buffer.peek();\n\t\tconst sleep = Time.Milli.add(Time.Milli.sub(currentRef, ref), floor);\n\t\tif (sleep < 0) {\n\t\t\tconst entry = this.#late.get(label);\n\t\t\tif (entry) {\n\t\t\t\tentry.count++;\n\t\t\t\tentry.maxMs = Math.max(entry.maxMs, -sleep);\n\t\t\t} else {\n\t\t\t\tthis.#late.set(label, { count: 1, maxMs: -sleep });\n\t\t\t}\n\t\t} else {\n\t\t\tconst entry = this.#late.get(label);\n\t\t\tif (entry) {\n\t\t\t\tconst prefix = label ? `sync[${label}]` : \"sync\";\n\t\t\t\tconst behind = Sync.#formatDuration(entry.maxMs);\n\t\t\t\tconsole.debug(`${prefix}: ${entry.count} late frame(s), max ${behind} behind`);\n\t\t\t\tthis.#late.delete(label);\n\t\t\t}\n\t\t}\n\n\t\t// Frame isn't earlier than the anchor: it can't lower latency, so keep the reference.\n\t\tif (ref >= currentRef) return;\n\n\t\t// Frame is earlier (more lookahead). `sleep` is the latency keeping the anchor would impose.\n\t\tconst cap = this.#latencyCap();\n\t\tif (sleep <= cap) return; // within budget: let the buffer grow instead of skipping ahead\n\n\t\t// Over the cap: re-anchor down so the resulting latency is exactly the cap.\n\t\tthis.#setReference(Time.Milli.add(ref, Time.Milli.sub(cap, floor)));\n\t}\n\n\t#setReference(ref: Time.Milli): void {\n\t\tthis.#out.reference.set(ref);\n\t\tthis.#update.resolve();\n\t\tthis.#update = Promise.withResolvers();\n\t}\n\n\t// Re-anchor playback to the next frame received. Call this at an utterance boundary\n\t// in buffered mode (typically alongside flushing the audio buffer) so the new content\n\t// plays from its own first frame instead of inheriting the previous reference.\n\treset(): void {\n\t\tthis.#out.reference.set(undefined);\n\t\tthis.#late.clear();\n\t\tthis.#update.resolve();\n\t\tthis.#update = Promise.withResolvers();\n\t}\n\n\t// The PTS that should be rendering right now, derived from the reference + buffer.\n\t// Returns undefined if no frames have been received yet.\n\tnow(): Time.Milli | undefined {\n\t\tconst reference = this.#out.reference.peek();\n\t\tif (reference === undefined) return undefined;\n\t\treturn Time.Milli.sub(Time.Milli.sub(Time.Milli.now(), reference), this.#out.buffer.peek());\n\t}\n\n\t// Sleep until it's time to render this frame.\n\tasync wait(timestamp: Time.Milli): Promise<void> {\n\t\tconst reference = this.#out.reference.peek();\n\t\tif (reference === undefined) {\n\t\t\tthrow new Error(\"reference not set; call received() first\");\n\t\t}\n\n\t\tfor (;;) {\n\t\t\t// Sleep until it's time to decode the next frame.\n\t\t\t// NOTE: This function runs in parallel for each frame.\n\t\t\tconst now = Time.Milli.now();\n\t\t\tconst ref = Time.Milli.sub(now, timestamp);\n\n\t\t\tconst currentRef = this.#out.reference.peek();\n\t\t\tif (currentRef === undefined) return;\n\n\t\t\tconst sleep = Time.Milli.add(Time.Milli.sub(currentRef, ref), this.#out.buffer.peek());\n\t\t\tif (sleep <= 0) return;\n\n\t\t\t// Skip setTimeout for small sleeps; the timer resolution (~4ms) would overshoot.\n\t\t\tif (sleep < 5) return;\n\n\t\t\tconst wait = new Promise((resolve) => setTimeout(resolve, sleep)).then(() => true);\n\n\t\t\tconst ok = await Promise.race([this.#update.promise, wait]);\n\t\t\tif (ok) return;\n\t\t}\n\t}\n\n\tstatic #formatDuration(ms: number): string {\n\t\tms = Math.round(ms);\n\t\tif (ms < 1000) return `${ms}ms`;\n\t\tconst s = ms / 1000;\n\t\tif (s < 60) return `${Math.round(s * 10) / 10}s`;\n\t\tconst m = s / 60;\n\t\treturn `${Math.round(m * 10) / 10}m`;\n\t}\n\n\tclose() {\n\t\tthis.#signals.close();\n\t}\n}\n"],"mappings":"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"}
package/ui/element.js CHANGED
@@ -627,7 +627,7 @@ function de(e, t) {
627
627
  }
628
628
  //#endregion
629
629
  //#region src/ui/styles/index.css?inline
630
- var fe = "*,:before,:after{box-sizing:border-box}:host{--color-white:#fff;--color-white-alpha-05:#ffffff0d;--color-white-alpha-10:#ffffff1a;--color-white-alpha-15:#ffffff26;--color-white-alpha-20:#fff3;--color-white-alpha-25:#ffffff40;--color-white-alpha-30:#ffffff4d;--color-white-alpha-40:#fff6;--color-white-alpha-60:#fff9;--color-white-alpha-70:#ffffffb3;--color-white-alpha-90:#ffffffe6;--color-gray-100:#9ca3af;--color-gray-400:#6b7280;--color-gray-950:#1a1a1a;--color-blue-400:#00dfff;--color-blue-500:#52a1ff;--color-blue-900:#1b3243;--color-purple-400:#a855f7;--color-purple-900:#31144a;--color-green-400:#65ee2e;--color-green-500:#22c55e;--color-green-900:#3d4928;--color-yellow-400:#facc15;--color-orange-400:#f90;--color-orange-450:#fb923c;--color-orange-500:#f97316;--color-orange-900:#4d301b;--color-red-400:#f87171;--color-amber-600:#d97706;--color-orange-500-alpha-60:#f9731699;--color-orange-500-alpha-80:#f97316cc;--color-black-alpha-08:#00000014;--color-black-alpha-10:#0000001a;--color-black-alpha-40:#0006;--color-black-alpha-60:#0009;--color-black-alpha-85:#000000d9;--color-panel:#121214eb;--spacing-1:.0625rem;--spacing-2:.125rem;--spacing-4:.25rem;--spacing-8:.5rem;--spacing-12:.75rem;--spacing-16:1rem;--spacing-24:1.5rem;--spacing-32:2rem;--spacing-48:3rem;--border-radius-sm:.375rem;--border-radius-md:.5rem;--border-radius-lg:.75rem;--accent:var(--color-orange-400);--ease:cubic-bezier(.4, 0, .2, 1);font-family:system-ui,Segoe UI,Roboto,sans-serif}.flex-center{justify-content:center;align-items:center;display:flex}.flex-align-center{align-items:center;display:flex}.control{width:2.25rem;height:2.25rem;color:var(--color-white);cursor:pointer;transition:background-color .12s var(--ease), transform .12s var(--ease), color .12s var(--ease);background:0 0;border:none;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;padding:0;display:inline-flex}.control svg{width:1.25rem;height:1.25rem}.control:hover:not(:disabled){background-color:var(--color-white-alpha-15)}.control:active:not(:disabled){transform:scale(.92)}.control--active{color:var(--accent)}.control:disabled{opacity:.4;cursor:default}.chip{padding:var(--spacing-4) var(--spacing-12);color:var(--color-white-alpha-70);background-color:var(--color-white-alpha-10);border:var(--spacing-1) solid transparent;cursor:pointer;transition:background-color .12s var(--ease), color .12s var(--ease), border-color .12s var(--ease);border-radius:999px;font-size:.75rem;font-weight:500}.chip:hover{background-color:var(--color-white-alpha-20);color:var(--color-white)}.chip--active{color:var(--color-gray-950);background-color:var(--accent);border-color:var(--accent)}:host{display:block;position:relative}.player{border-radius:var(--spacing-8);background:#000;justify-content:center;align-items:center;width:100%;line-height:0;display:flex;position:relative;overflow:hidden}.player ::slotted(moq-watch){width:100%;line-height:0;display:block}.player:not(.player--chrome){cursor:none}.player:fullscreen,.player--pseudo-fullscreen{border-radius:0;justify-content:center;align-items:center;display:flex}.player:-webkit-full-screen{border-radius:0;justify-content:center;align-items:center;display:flex}.player--pseudo-fullscreen{z-index:2147483647;width:100vw;height:100vh;position:fixed;inset:0}@supports (height:100dvh){.player--pseudo-fullscreen{height:100dvh}}.player:fullscreen ::slotted(moq-watch),.player--pseudo-fullscreen ::slotted(moq-watch){width:100%;height:100%;max-height:100%}.player:-webkit-full-screen ::slotted(moq-watch){width:100%;height:100%;max-height:100%}.scrim{pointer-events:none;opacity:0;transition:opacity .2s var(--ease);position:absolute;left:0;right:0}.scrim--top{background:linear-gradient(to bottom, var(--color-black-alpha-40), transparent);height:25%;top:0}.scrim--bottom{background:linear-gradient(to top, var(--color-black-alpha-60), transparent 60%);height:100%;bottom:0}.player--chrome .scrim{opacity:1}.center{pointer-events:none;justify-content:center;align-items:center;line-height:1;display:flex;position:absolute;inset:0}.center-play{pointer-events:auto;width:4.5rem;height:4.5rem;color:var(--color-white);background:linear-gradient(to bottom right, var(--color-orange-400), var(--color-orange-500), var(--color-amber-600));cursor:pointer;box-shadow:0 var(--spacing-8) var(--spacing-24) var(--color-black-alpha-40);transition:transform .15s var(--ease), box-shadow .15s var(--ease);border:none;border-radius:50%}.center-play svg{width:2rem;height:2rem}.center-play:hover{box-shadow:0 var(--spacing-8) var(--spacing-32) var(--color-orange-500-alpha-60);transform:scale(1.06)}.center-play:active{transform:scale(.97)}.chrome{opacity:0;pointer-events:none;transition:opacity .2s var(--ease), transform .2s var(--ease);flex-direction:column;justify-content:flex-end;line-height:1.4;display:flex;position:absolute;bottom:0;left:0;right:0;transform:translateY(.5rem)}.player--chrome .chrome{opacity:1;pointer-events:auto;transform:translateY(0)}.controls{align-items:center;gap:var(--spacing-4);padding:var(--spacing-8) var(--spacing-12) var(--spacing-12);display:flex;position:relative}.controls-group{align-items:center;gap:var(--spacing-4);display:flex}.controls-spacer{flex:1}.volume{gap:var(--spacing-4)}.volume-track{appearance:none;background:linear-gradient(to right, var(--color-white) 0%, var(--color-white) var(--fill,50%), var(--color-white-alpha-30) var(--fill,50%));cursor:pointer;opacity:0;width:0;max-width:0;height:.25rem;transition:width .16s var(--ease), max-width .16s var(--ease), opacity .16s var(--ease);border-radius:999px;outline:none;margin:0}.volume:hover .volume-track,.volume:focus-within .volume-track{opacity:1;width:4.5rem;max-width:4.5rem}.volume-track::-webkit-slider-thumb{appearance:none;background:var(--color-white);cursor:pointer;border:none;border-radius:50%;width:.75rem;height:.75rem}.volume-track::-moz-range-thumb{background:var(--color-white);cursor:pointer;border:none;border-radius:50%;width:.75rem;height:.75rem}.badge{gap:var(--spacing-8);height:2.25rem;padding:0 var(--spacing-12);margin-left:var(--spacing-4);color:var(--color-white);border-radius:var(--border-radius-sm);cursor:pointer;transition:background-color .12s var(--ease);background:0 0;border:none;font-size:.8125rem}.badge:hover{background-color:var(--color-white-alpha-10)}.badge-dot{background:var(--color-gray-400);border-radius:50%;flex-shrink:0;width:.5rem;height:.5rem}.badge-text{letter-spacing:.04em;font-weight:600}.badge-latency{max-width:0;margin-left:calc(-1 * var(--spacing-8));opacity:0;white-space:nowrap;font-variant-numeric:tabular-nums;color:var(--color-white-alpha-60);transition:max-width .16s var(--ease), margin-left .16s var(--ease), opacity .16s var(--ease);overflow:hidden}.badge:hover .badge-latency,.badge:focus-visible .badge-latency{opacity:1;max-width:9rem;margin-left:0}.badge[data-variant=live] .badge-dot{background:var(--color-red-400);animation:badge-pulse 2s var(--ease) infinite;box-shadow:0 0 #f87171b3}.badge[data-variant=live] .badge-text{color:var(--color-red-400)}.badge[data-variant=loading] .badge-dot,.badge[data-variant=connecting] .badge-dot{background:var(--color-yellow-400)}.badge[data-variant=error] .badge-dot{background:var(--color-gray-400)}.badge[data-variant=error] .badge-text{color:var(--color-gray-100)}@keyframes badge-pulse{0%{box-shadow:0 0 #f8717199}70%{box-shadow:0 0 0 .4rem #f8717100}to{box-shadow:0 0 #f8717100}}.panel{top:var(--spacing-8);right:var(--spacing-8);bottom:var(--spacing-8);width:20rem;max-width:calc(100% - var(--spacing-16));background:var(--color-panel);backdrop-filter:blur(12px);border:var(--spacing-1) solid var(--color-white-alpha-15);border-radius:var(--border-radius-lg);box-shadow:0 var(--spacing-8) var(--spacing-32) var(--color-black-alpha-60);color:var(--color-white);z-index:4;opacity:0;pointer-events:none;transition:opacity .2s var(--ease), transform .2s var(--ease);flex-direction:column;line-height:1.4;display:flex;position:absolute;overflow:hidden;transform:translate(.75rem)}.panel--open{opacity:1;pointer-events:auto;transform:translate(0)}.panel-head{padding:var(--spacing-8) var(--spacing-8) var(--spacing-8) var(--spacing-16);border-bottom:var(--spacing-1) solid var(--color-white-alpha-10);justify-content:space-between;align-items:center;display:flex}.panel-head-title{gap:var(--spacing-8);font-size:.875rem;font-weight:600}.panel-head-title svg{width:1.125rem;height:1.125rem}.panel-head .control{width:2rem;height:2rem}.panel-tabs{padding:var(--spacing-8);gap:var(--spacing-4);display:flex}.panel-tab{align-items:center;gap:var(--spacing-4);padding:var(--spacing-8) var(--spacing-4);color:var(--color-white-alpha-60);border-radius:var(--border-radius-md);cursor:pointer;transition:background-color .12s var(--ease), color .12s var(--ease);background:0 0;border:none;flex-direction:column;flex:1;font-size:.6875rem;font-weight:500;display:flex}.panel-tab svg{width:1.125rem;height:1.125rem}.panel-tab:hover{background-color:var(--color-white-alpha-05);color:var(--color-white)}.panel-tab--active{background-color:var(--color-white-alpha-10);color:var(--color-white)}.panel-body{scrollbar-width:thin;scrollbar-color:var(--color-white-alpha-20) transparent;flex:1;min-height:0;overflow-y:auto}.panel-body::-webkit-scrollbar{width:.375rem}.panel-body::-webkit-scrollbar-thumb{background:var(--color-white-alpha-20);border-radius:999px}.tab-body{gap:var(--spacing-12);padding:var(--spacing-12) var(--spacing-16) var(--spacing-16);flex-direction:column;display:flex}.tab-empty{padding:var(--spacing-24) 0;text-align:center;color:var(--color-white-alpha-40);font-size:.8125rem}.tab-hint{color:var(--color-white-alpha-40);font-size:.6875rem;line-height:1.5}.q-list{gap:var(--spacing-2)}.q-row{align-items:center;gap:var(--spacing-8);width:100%;padding:var(--spacing-8);text-align:left;color:var(--color-white);border-radius:var(--border-radius-md);cursor:pointer;transition:background-color .12s var(--ease);background:0 0;border:none;display:flex}.q-row:hover{background-color:var(--color-white-alpha-05)}.q-row--selected{background-color:var(--color-white-alpha-10)}.q-row-check{width:1rem;color:var(--accent);flex-shrink:0;justify-content:center;align-items:center;display:flex}.q-row-check svg{width:1rem;height:1rem}.q-row-body{flex-direction:column;gap:.1rem;min-width:0;display:flex}.q-row-title{align-items:center;gap:var(--spacing-8);font-size:.8125rem;font-weight:600;display:flex}.q-row-playing{letter-spacing:.06em;text-transform:uppercase;color:var(--color-green-500);background:var(--color-green-900);padding:.05rem var(--spacing-4);border-radius:var(--border-radius-sm);font-size:.5625rem;font-weight:600}.q-row-sub{color:var(--color-white-alpha-60);text-overflow:ellipsis;white-space:nowrap;font-size:.6875rem;overflow:hidden}.latency-presets{gap:var(--spacing-4);flex-wrap:wrap;display:flex}.latency-readout{gap:var(--spacing-8);display:flex}.latency-stat{gap:var(--spacing-2);padding:var(--spacing-8) var(--spacing-12);background:var(--color-white-alpha-05);border-radius:var(--border-radius-md);flex-direction:column;flex:1;display:flex}.latency-stat-label{text-transform:uppercase;letter-spacing:.05em;color:var(--color-white-alpha-40);font-size:.625rem}.latency-stat-value{font-variant-numeric:tabular-nums;color:var(--accent);font-size:.9375rem;font-weight:600}.stat-card{gap:var(--spacing-8);padding:var(--spacing-12);background:var(--color-white-alpha-05);border-radius:var(--border-radius-md);flex-direction:column;display:flex}.stat-head{align-items:center;gap:var(--spacing-8);display:flex}.stat-icon{border-radius:var(--border-radius-sm);justify-content:center;align-items:center;width:1.5rem;height:1.5rem;display:flex}.stat-icon svg{width:.875rem;height:.875rem}.stat-title{text-transform:uppercase;letter-spacing:.05em;font-size:.75rem;font-weight:600}.stat-status{letter-spacing:.06em;text-transform:uppercase;color:var(--color-gray-100);background:var(--color-white-alpha-10);padding:.05rem var(--spacing-8);border-radius:var(--border-radius-sm);margin-left:auto;font-size:.5625rem;font-weight:600}.stat-card--video .stat-icon{background:var(--color-purple-900);color:var(--color-purple-400)}.stat-card--video .stat-title{color:var(--color-purple-400)}.stat-card--audio .stat-icon{background:var(--color-green-900);color:var(--color-green-400)}.stat-card--audio .stat-title{color:var(--color-green-400)}.stat-card--network .stat-icon{background:var(--color-blue-900);color:var(--color-blue-400)}.stat-card--network .stat-title{color:var(--color-blue-400)}.stat-grid{gap:var(--spacing-2) var(--spacing-12);grid-template-columns:auto 1fr;display:grid}.stat-line{display:contents}.stat-key{color:var(--color-white-alpha-60);font-size:.75rem}.stat-value{text-align:right;font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap;font-size:.75rem;font-weight:600;overflow:hidden}.graph{gap:var(--spacing-2);margin-top:var(--spacing-4);flex-direction:column;display:flex}.graph-header{justify-content:space-between;align-items:baseline;display:flex}.graph-label{text-transform:uppercase;letter-spacing:.05em;color:var(--color-white-alpha-40);font-size:.625rem}.graph-value{font-variant-numeric:tabular-nums;font-size:.6875rem;font-weight:600}.graph-canvas{width:100%;height:2.5rem;display:block}.buffer{width:100%}.buffer-visualization{background:var(--color-white-alpha-10);border-radius:var(--spacing-4);cursor:pointer;user-select:none;width:100%;height:3.75rem;padding-left:var(--spacing-48);box-sizing:border-box;position:relative}.buffer-playhead{left:var(--spacing-48);width:var(--spacing-2);background:var(--color-white);z-index:3;position:absolute;top:0;bottom:0}.buffer-track{left:var(--spacing-48);height:var(--spacing-24);align-items:center;display:flex;position:absolute;right:0}.buffer-track--video{top:var(--spacing-4)}.buffer-track--audio{top:var(--spacing-32)}.buffer-track-label{width:2.5rem;color:var(--color-white-alpha-70);text-align:right;padding-right:var(--spacing-4);pointer-events:none;box-sizing:border-box;font-size:.625rem;position:absolute;left:-2.875rem}.buffer-canvas{width:100%;height:100%;position:absolute;top:0;left:0}.buffer-target-area{top:0;bottom:0;left:var(--spacing-48);pointer-events:none;position:absolute;right:0}.buffer-target-line{background:var(--color-orange-400,var(--color-orange-450));z-index:2;cursor:ew-resize;border-radius:var(--spacing-1);pointer-events:auto;width:.1875rem;position:absolute;top:0;bottom:0}.buffer-target-label{color:var(--color-orange-400,var(--color-orange-450));white-space:nowrap;margin-bottom:var(--spacing-2);opacity:0;pointer-events:none;font-size:.6875rem;font-weight:500;transition:opacity .15s;position:absolute;bottom:100%;left:50%;transform:translate(-50%)}.buffer-visualization:hover .buffer-target-label,.buffer-visualization--dragging .buffer-target-label{opacity:1}.buffer-help{color:var(--color-white-alpha-30,#ffffff4d);pointer-events:none;white-space:nowrap;font-size:.75rem;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.buffering{z-index:1;background-color:var(--color-black-alpha-40);width:100%;height:100%;backdrop-filter:blur(var(--spacing-2));pointer-events:auto;justify-content:center;align-items:center;display:flex;position:absolute;top:0;left:0}.buffering-spinner{border:var(--spacing-4) solid var(--color-white-alpha-20);border-top:var(--spacing-4) solid var(--color-white);border-radius:50%;width:2.5rem;height:2.5rem;animation:1s linear infinite buffer-spin}@keyframes buffer-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.watch-ui__offline-indicator{z-index:4;pointer-events:auto;background-color:#000;border-radius:.25rem;justify-content:center;align-items:center;padding:.5rem 1rem;display:flex;position:absolute;bottom:1rem;right:1rem}.watch-ui__offline-text{color:#fff;letter-spacing:.08em;text-shadow:none;font-size:.875rem;font-weight:700}@media (width<=768px){.watch-ui__offline-indicator{padding:.375rem .75rem;bottom:.75rem;right:.75rem}.watch-ui__offline-text{font-size:.75rem}}@media (width<=480px){.watch-ui__offline-indicator{padding:.25rem .5rem;bottom:.5rem;right:.5rem}.watch-ui__offline-text{font-size:.75rem}}.watch-ui__unsupported-indicator{text-align:center;background-color:var(--color-black-alpha-85);z-index:4;pointer-events:auto;border-radius:.25rem;justify-content:center;align-items:center;max-width:min(80%,32rem);padding:.75rem 1.25rem;display:flex;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.watch-ui__unsupported-text{color:var(--color-white);font-size:.875rem;font-weight:600;line-height:1.4}@media (width<=480px){.watch-ui__unsupported-indicator{max-width:90%;padding:.625rem .875rem}.watch-ui__unsupported-text{font-size:.75rem}}", pe = 2800;
630
+ var fe = "*,:before,:after{box-sizing:border-box}:host{--color-white:#fff;--color-white-alpha-05:#ffffff0d;--color-white-alpha-10:#ffffff1a;--color-white-alpha-15:#ffffff26;--color-white-alpha-20:#fff3;--color-white-alpha-25:#ffffff40;--color-white-alpha-30:#ffffff4d;--color-white-alpha-40:#fff6;--color-white-alpha-60:#fff9;--color-white-alpha-70:#ffffffb3;--color-white-alpha-90:#ffffffe6;--color-gray-100:#9ca3af;--color-gray-400:#6b7280;--color-gray-950:#1a1a1a;--color-blue-400:#00dfff;--color-blue-500:#52a1ff;--color-blue-900:#1b3243;--color-purple-400:#a855f7;--color-purple-900:#31144a;--color-green-400:#65ee2e;--color-green-500:#22c55e;--color-green-900:#3d4928;--color-yellow-400:#facc15;--color-orange-400:#f90;--color-orange-450:#fb923c;--color-orange-500:#f97316;--color-orange-900:#4d301b;--color-red-400:#f87171;--color-amber-600:#d97706;--color-orange-500-alpha-60:#f9731699;--color-orange-500-alpha-80:#f97316cc;--color-black-alpha-08:#00000014;--color-black-alpha-10:#0000001a;--color-black-alpha-40:#0006;--color-black-alpha-60:#0009;--color-black-alpha-85:#000000d9;--color-panel:#121214eb;--spacing-1:.0625rem;--spacing-2:.125rem;--spacing-4:.25rem;--spacing-8:.5rem;--spacing-12:.75rem;--spacing-16:1rem;--spacing-24:1.5rem;--spacing-32:2rem;--spacing-48:3rem;--border-radius-sm:.375rem;--border-radius-md:.5rem;--border-radius-lg:.75rem;--accent:var(--color-orange-400);--ease:cubic-bezier(.4, 0, .2, 1);font-family:system-ui,Segoe UI,Roboto,sans-serif}.flex-center{justify-content:center;align-items:center;display:flex}.flex-align-center{align-items:center;display:flex}.control{width:2.25rem;height:2.25rem;color:var(--color-white);cursor:pointer;transition:background-color .12s var(--ease), transform .12s var(--ease), color .12s var(--ease);background:0 0;border:none;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;padding:0;display:inline-flex}.control svg{width:1.25rem;height:1.25rem}.control:hover:not(:disabled){background-color:var(--color-white-alpha-15)}.control:active:not(:disabled){transform:scale(.92)}.control--active{color:var(--accent)}.control:disabled{opacity:.4;cursor:default}.chip{padding:var(--spacing-4) var(--spacing-12);color:var(--color-white-alpha-70);background-color:var(--color-white-alpha-10);border:var(--spacing-1) solid transparent;cursor:pointer;transition:background-color .12s var(--ease), color .12s var(--ease), border-color .12s var(--ease);border-radius:999px;font-size:.75rem;font-weight:500}.chip:hover{background-color:var(--color-white-alpha-20);color:var(--color-white)}.chip--active{color:var(--color-gray-950);background-color:var(--accent);border-color:var(--accent)}:host{display:block;position:relative}.player{border-radius:var(--spacing-8);background:#000;justify-content:center;align-items:center;width:100%;line-height:0;display:flex;position:relative;overflow:hidden}.player ::slotted(moq-watch){width:100%;line-height:0;display:block}.player:not(.player--chrome){cursor:none}.player:fullscreen,.player:-webkit-full-screen,.player--pseudo-fullscreen{border-radius:0;justify-content:center;align-items:center;display:flex}.player:fullscreen,.player:fullscreen,.player--pseudo-fullscreen{border-radius:0;justify-content:center;align-items:center;display:flex}.player--pseudo-fullscreen{z-index:2147483647;width:100vw;height:100vh;position:fixed;inset:0}@supports (height:100dvh){.player--pseudo-fullscreen{height:100dvh}}.player:fullscreen ::slotted(moq-watch),.player:-webkit-full-screen ::slotted(moq-watch),.player--pseudo-fullscreen ::slotted(moq-watch){width:100%;height:100%;max-height:100%}.player:fullscreen ::slotted(moq-watch),.player:fullscreen ::slotted(moq-watch),.player--pseudo-fullscreen ::slotted(moq-watch){width:100%;height:100%;max-height:100%}.scrim{pointer-events:none;opacity:0;transition:opacity .2s var(--ease);position:absolute;left:0;right:0}.scrim--top{background:linear-gradient(to bottom, var(--color-black-alpha-40), transparent);height:25%;top:0}.scrim--bottom{background:linear-gradient(to top, var(--color-black-alpha-60), transparent 60%);height:100%;bottom:0}.player--chrome .scrim{opacity:1}.center{pointer-events:none;justify-content:center;align-items:center;line-height:1;display:flex;position:absolute;inset:0}.center-play{pointer-events:auto;width:4.5rem;height:4.5rem;color:var(--color-white);background:linear-gradient(to bottom right, var(--color-orange-400), var(--color-orange-500), var(--color-amber-600));cursor:pointer;box-shadow:0 var(--spacing-8) var(--spacing-24) var(--color-black-alpha-40);transition:transform .15s var(--ease), box-shadow .15s var(--ease);border:none;border-radius:50%}.center-play svg{width:2rem;height:2rem}.center-play:hover{box-shadow:0 var(--spacing-8) var(--spacing-32) var(--color-orange-500-alpha-60);transform:scale(1.06)}.center-play:active{transform:scale(.97)}.chrome{opacity:0;pointer-events:none;transition:opacity .2s var(--ease), transform .2s var(--ease);flex-direction:column;justify-content:flex-end;line-height:1.4;display:flex;position:absolute;bottom:0;left:0;right:0;transform:translateY(.5rem)}.player--chrome .chrome{opacity:1;pointer-events:auto;transform:translateY(0)}.controls{align-items:center;gap:var(--spacing-4);padding:var(--spacing-8) var(--spacing-12) var(--spacing-12);display:flex;position:relative}.controls-group{align-items:center;gap:var(--spacing-4);display:flex}.controls-spacer{flex:1}.volume{gap:var(--spacing-4)}.volume-track{-webkit-appearance:none;appearance:none;background:linear-gradient(to right, var(--color-white) 0%, var(--color-white) var(--fill,50%), var(--color-white-alpha-30) var(--fill,50%));cursor:pointer;opacity:0;width:0;max-width:0;height:.25rem;transition:width .16s var(--ease), max-width .16s var(--ease), opacity .16s var(--ease);border-radius:999px;outline:none;margin:0}.volume:hover .volume-track,.volume:focus-within .volume-track{opacity:1;width:4.5rem;max-width:4.5rem}.volume-track::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;background:var(--color-white);cursor:pointer;border:none;border-radius:50%;width:.75rem;height:.75rem}.volume-track::-moz-range-thumb{background:var(--color-white);cursor:pointer;border:none;border-radius:50%;width:.75rem;height:.75rem}.badge{gap:var(--spacing-8);height:2.25rem;padding:0 var(--spacing-12);margin-left:var(--spacing-4);color:var(--color-white);border-radius:var(--border-radius-sm);cursor:pointer;transition:background-color .12s var(--ease);background:0 0;border:none;font-size:.8125rem}.badge:hover{background-color:var(--color-white-alpha-10)}.badge-dot{background:var(--color-gray-400);border-radius:50%;flex-shrink:0;width:.5rem;height:.5rem}.badge-text{letter-spacing:.04em;font-weight:600}.badge-latency{max-width:0;margin-left:calc(-1 * var(--spacing-8));opacity:0;white-space:nowrap;font-variant-numeric:tabular-nums;color:var(--color-white-alpha-60);transition:max-width .16s var(--ease), margin-left .16s var(--ease), opacity .16s var(--ease);overflow:hidden}.badge:hover .badge-latency,.badge:focus-visible .badge-latency{opacity:1;max-width:9rem;margin-left:0}.badge[data-variant=live] .badge-dot{background:var(--color-red-400);animation:badge-pulse 2s var(--ease) infinite;box-shadow:0 0 #f87171b3}.badge[data-variant=live] .badge-text{color:var(--color-red-400)}.badge[data-variant=loading] .badge-dot,.badge[data-variant=connecting] .badge-dot{background:var(--color-yellow-400)}.badge[data-variant=error] .badge-dot{background:var(--color-gray-400)}.badge[data-variant=error] .badge-text{color:var(--color-gray-100)}@keyframes badge-pulse{0%{box-shadow:0 0 #f8717199}70%{box-shadow:0 0 0 .4rem #f8717100}to{box-shadow:0 0 #f8717100}}.panel{top:var(--spacing-8);right:var(--spacing-8);bottom:var(--spacing-8);width:20rem;max-width:calc(100% - var(--spacing-16));background:var(--color-panel);backdrop-filter:blur(12px);border:var(--spacing-1) solid var(--color-white-alpha-15);border-radius:var(--border-radius-lg);box-shadow:0 var(--spacing-8) var(--spacing-32) var(--color-black-alpha-60);color:var(--color-white);z-index:4;opacity:0;pointer-events:none;transition:opacity .2s var(--ease), transform .2s var(--ease);flex-direction:column;line-height:1.4;display:flex;position:absolute;overflow:hidden;transform:translate(.75rem)}.panel--open{opacity:1;pointer-events:auto;transform:translate(0)}.panel-head{padding:var(--spacing-8) var(--spacing-8) var(--spacing-8) var(--spacing-16);border-bottom:var(--spacing-1) solid var(--color-white-alpha-10);justify-content:space-between;align-items:center;display:flex}.panel-head-title{gap:var(--spacing-8);font-size:.875rem;font-weight:600}.panel-head-title svg{width:1.125rem;height:1.125rem}.panel-head .control{width:2rem;height:2rem}.panel-tabs{padding:var(--spacing-8);gap:var(--spacing-4);display:flex}.panel-tab{align-items:center;gap:var(--spacing-4);padding:var(--spacing-8) var(--spacing-4);color:var(--color-white-alpha-60);border-radius:var(--border-radius-md);cursor:pointer;transition:background-color .12s var(--ease), color .12s var(--ease);background:0 0;border:none;flex-direction:column;flex:1;font-size:.6875rem;font-weight:500;display:flex}.panel-tab svg{width:1.125rem;height:1.125rem}.panel-tab:hover{background-color:var(--color-white-alpha-05);color:var(--color-white)}.panel-tab--active{background-color:var(--color-white-alpha-10);color:var(--color-white)}.panel-body{scrollbar-width:thin;scrollbar-color:var(--color-white-alpha-20) transparent;flex:1;min-height:0;overflow-y:auto}.panel-body::-webkit-scrollbar{width:.375rem}.panel-body::-webkit-scrollbar-thumb{background:var(--color-white-alpha-20);border-radius:999px}.tab-body{gap:var(--spacing-12);padding:var(--spacing-12) var(--spacing-16) var(--spacing-16);flex-direction:column;display:flex}.tab-empty{padding:var(--spacing-24) 0;text-align:center;color:var(--color-white-alpha-40);font-size:.8125rem}.tab-hint{color:var(--color-white-alpha-40);font-size:.6875rem;line-height:1.5}.q-list{gap:var(--spacing-2)}.q-row{align-items:center;gap:var(--spacing-8);width:100%;padding:var(--spacing-8);text-align:left;color:var(--color-white);border-radius:var(--border-radius-md);cursor:pointer;transition:background-color .12s var(--ease);background:0 0;border:none;display:flex}.q-row:hover{background-color:var(--color-white-alpha-05)}.q-row--selected{background-color:var(--color-white-alpha-10)}.q-row-check{width:1rem;color:var(--accent);flex-shrink:0;justify-content:center;align-items:center;display:flex}.q-row-check svg{width:1rem;height:1rem}.q-row-body{flex-direction:column;gap:.1rem;min-width:0;display:flex}.q-row-title{align-items:center;gap:var(--spacing-8);font-size:.8125rem;font-weight:600;display:flex}.q-row-playing{letter-spacing:.06em;text-transform:uppercase;color:var(--color-green-500);background:var(--color-green-900);padding:.05rem var(--spacing-4);border-radius:var(--border-radius-sm);font-size:.5625rem;font-weight:600}.q-row-sub{color:var(--color-white-alpha-60);text-overflow:ellipsis;white-space:nowrap;font-size:.6875rem;overflow:hidden}.latency-presets{gap:var(--spacing-4);flex-wrap:wrap;display:flex}.latency-readout{gap:var(--spacing-8);display:flex}.latency-stat{gap:var(--spacing-2);padding:var(--spacing-8) var(--spacing-12);background:var(--color-white-alpha-05);border-radius:var(--border-radius-md);flex-direction:column;flex:1;display:flex}.latency-stat-label{text-transform:uppercase;letter-spacing:.05em;color:var(--color-white-alpha-40);font-size:.625rem}.latency-stat-value{font-variant-numeric:tabular-nums;color:var(--accent);font-size:.9375rem;font-weight:600}.stat-card{gap:var(--spacing-8);padding:var(--spacing-12);background:var(--color-white-alpha-05);border-radius:var(--border-radius-md);flex-direction:column;display:flex}.stat-head{align-items:center;gap:var(--spacing-8);display:flex}.stat-icon{border-radius:var(--border-radius-sm);justify-content:center;align-items:center;width:1.5rem;height:1.5rem;display:flex}.stat-icon svg{width:.875rem;height:.875rem}.stat-title{text-transform:uppercase;letter-spacing:.05em;font-size:.75rem;font-weight:600}.stat-status{letter-spacing:.06em;text-transform:uppercase;color:var(--color-gray-100);background:var(--color-white-alpha-10);padding:.05rem var(--spacing-8);border-radius:var(--border-radius-sm);margin-left:auto;font-size:.5625rem;font-weight:600}.stat-card--video .stat-icon{background:var(--color-purple-900);color:var(--color-purple-400)}.stat-card--video .stat-title{color:var(--color-purple-400)}.stat-card--audio .stat-icon{background:var(--color-green-900);color:var(--color-green-400)}.stat-card--audio .stat-title{color:var(--color-green-400)}.stat-card--network .stat-icon{background:var(--color-blue-900);color:var(--color-blue-400)}.stat-card--network .stat-title{color:var(--color-blue-400)}.stat-grid{gap:var(--spacing-2) var(--spacing-12);grid-template-columns:auto 1fr;display:grid}.stat-line{display:contents}.stat-key{color:var(--color-white-alpha-60);font-size:.75rem}.stat-value{text-align:right;font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap;font-size:.75rem;font-weight:600;overflow:hidden}.graph{gap:var(--spacing-2);margin-top:var(--spacing-4);flex-direction:column;display:flex}.graph-header{justify-content:space-between;align-items:baseline;display:flex}.graph-label{text-transform:uppercase;letter-spacing:.05em;color:var(--color-white-alpha-40);font-size:.625rem}.graph-value{font-variant-numeric:tabular-nums;font-size:.6875rem;font-weight:600}.graph-canvas{width:100%;height:2.5rem;display:block}.buffer{width:100%}.buffer-visualization{background:var(--color-white-alpha-10);border-radius:var(--spacing-4);cursor:pointer;user-select:none;width:100%;height:3.75rem;padding-left:var(--spacing-48);box-sizing:border-box;position:relative}.buffer-playhead{left:var(--spacing-48);width:var(--spacing-2);background:var(--color-white);z-index:3;position:absolute;top:0;bottom:0}.buffer-track{left:var(--spacing-48);height:var(--spacing-24);align-items:center;display:flex;position:absolute;right:0}.buffer-track--video{top:var(--spacing-4)}.buffer-track--audio{top:var(--spacing-32)}.buffer-track-label{width:2.5rem;color:var(--color-white-alpha-70);text-align:right;padding-right:var(--spacing-4);pointer-events:none;box-sizing:border-box;font-size:.625rem;position:absolute;left:-2.875rem}.buffer-canvas{width:100%;height:100%;position:absolute;top:0;left:0}.buffer-target-area{top:0;bottom:0;left:var(--spacing-48);pointer-events:none;position:absolute;right:0}.buffer-target-line{background:var(--color-orange-400,var(--color-orange-450));z-index:2;cursor:ew-resize;border-radius:var(--spacing-1);pointer-events:auto;width:.1875rem;position:absolute;top:0;bottom:0}.buffer-target-label{color:var(--color-orange-400,var(--color-orange-450));white-space:nowrap;margin-bottom:var(--spacing-2);opacity:0;pointer-events:none;font-size:.6875rem;font-weight:500;transition:opacity .15s;position:absolute;bottom:100%;left:50%;transform:translate(-50%)}.buffer-visualization:hover .buffer-target-label,.buffer-visualization--dragging .buffer-target-label{opacity:1}.buffer-help{color:var(--color-white-alpha-30,#ffffff4d);pointer-events:none;white-space:nowrap;font-size:.75rem;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.buffering{z-index:1;background-color:var(--color-black-alpha-40);width:100%;height:100%;backdrop-filter:blur(var(--spacing-2));pointer-events:auto;justify-content:center;align-items:center;display:flex;position:absolute;top:0;left:0}.buffering-spinner{border:var(--spacing-4) solid var(--color-white-alpha-20);border-top:var(--spacing-4) solid var(--color-white);border-radius:50%;width:2.5rem;height:2.5rem;animation:1s linear infinite buffer-spin}@keyframes buffer-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.watch-ui__offline-indicator{z-index:4;pointer-events:auto;background-color:#000;border-radius:.25rem;justify-content:center;align-items:center;padding:.5rem 1rem;display:flex;position:absolute;bottom:1rem;right:1rem}.watch-ui__offline-text{color:#fff;letter-spacing:.08em;text-shadow:none;font-size:.875rem;font-weight:700}@media (width<=768px){.watch-ui__offline-indicator{padding:.375rem .75rem;bottom:.75rem;right:.75rem}.watch-ui__offline-text{font-size:.75rem}}@media (width<=480px){.watch-ui__offline-indicator{padding:.25rem .5rem;bottom:.5rem;right:.5rem}.watch-ui__offline-text{font-size:.75rem}}.watch-ui__unsupported-indicator{text-align:center;background-color:var(--color-black-alpha-85);z-index:4;pointer-events:auto;border-radius:.25rem;justify-content:center;align-items:center;max-width:min(80%,32rem);padding:.75rem 1.25rem;display:flex;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.watch-ui__unsupported-text{color:var(--color-white);font-size:.875rem;font-weight:600;line-height:1.4}@media (width<=480px){.watch-ui__unsupported-indicator{max-width:90%;padding:.625rem .875rem}.watch-ui__unsupported-text{font-size:.75rem}}", pe = 2800;
631
631
  function me(e) {
632
632
  for (let t of [":fullscreen", ":-webkit-full-screen"]) try {
633
633
  if (e.matches(t)) return !0;