@moq/watch 0.2.15 → 0.2.16
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.
- package/broadcast-CG8jZRfA.js +1651 -0
- package/broadcast-CG8jZRfA.js.map +1 -0
- package/element.js +1 -1
- package/index.js +1 -1
- package/package.json +3 -3
- package/support/element.js +39 -39
- package/support/element.js.map +1 -1
- package/ui/components/button.d.ts +3 -0
- package/ui/components/button.d.ts.map +1 -0
- package/ui/components/center-play.d.ts +5 -0
- package/ui/components/center-play.d.ts.map +1 -0
- package/ui/components/control-bar.d.ts +6 -0
- package/ui/components/control-bar.d.ts.map +1 -0
- package/ui/components/fullscreen-button.d.ts +2 -1
- package/ui/components/fullscreen-button.d.ts.map +1 -1
- package/ui/components/latency.d.ts +5 -0
- package/ui/components/latency.d.ts.map +1 -0
- package/ui/components/live-badge.d.ts +9 -0
- package/ui/components/live-badge.d.ts.map +1 -0
- package/ui/components/play-pause.d.ts +1 -0
- package/ui/components/play-pause.d.ts.map +1 -1
- package/ui/components/quality.d.ts +5 -0
- package/ui/components/quality.d.ts.map +1 -0
- package/ui/components/settings-button.d.ts +5 -0
- package/ui/components/settings-button.d.ts.map +1 -0
- package/ui/components/settings-panel.d.ts +6 -0
- package/ui/components/settings-panel.d.ts.map +1 -0
- package/ui/components/volume-slider.d.ts +1 -0
- package/ui/components/volume-slider.d.ts.map +1 -1
- package/ui/element.d.ts.map +1 -1
- package/ui/element.js +577 -260
- package/ui/element.js.map +1 -1
- package/ui/format.d.ts +11 -0
- package/ui/format.d.ts.map +1 -0
- package/ui/fullscreen.d.ts +16 -0
- package/ui/fullscreen.d.ts.map +1 -0
- package/ui/graph.d.ts +18 -0
- package/ui/graph.d.ts.map +1 -0
- package/ui/icons.d.ts +5 -1
- package/ui/icons.d.ts.map +1 -1
- package/ui/state.d.ts +9 -0
- package/ui/state.d.ts.map +1 -0
- package/ui/stats.d.ts +3 -2
- package/ui/stats.d.ts.map +1 -1
- package/broadcast-jtLUfuyL.js +0 -4291
- package/broadcast-jtLUfuyL.js.map +0 -1
- package/dom-Yz3UT5ap.js +0 -29
- package/dom-Yz3UT5ap.js.map +0 -1
- package/libav-opus-af-CzuaKpJb.js +0 -252
- package/libav-opus-af-CzuaKpJb.js.map +0 -1
- package/main-bFFHfuX4.js +0 -1999
- package/main-bFFHfuX4.js.map +0 -1
- package/ui/components/quality-selector.d.ts +0 -4
- package/ui/components/quality-selector.d.ts.map +0 -1
- package/ui/components/stats-button.d.ts +0 -3
- package/ui/components/stats-button.d.ts.map +0 -1
- package/ui/components/watch-status-indicator.d.ts +0 -4
- package/ui/components/watch-status-indicator.d.ts.map +0 -1
package/ui/element.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"element.js","names":["#root","#observer","#updateWatch","#signals","#render","#watch"],"sources":["../../src/ui/components/buffer-control.ts","../../src/ui/components/buffering-indicator.ts","../../src/ui/icons.ts","../../src/ui/components/fullscreen-button.ts","../../src/ui/components/offline-indicator.ts","../../src/ui/components/play-pause.ts","../../src/ui/components/quality-selector.ts","../../src/ui/components/stats-button.ts","../../src/ui/components/volume-slider.ts","../../src/ui/components/watch-status-indicator.ts","../../src/ui/stats.ts","../../src/ui/styles/index.css?inline","../../src/ui/element.ts"],"sourcesContent":["import type { Moq } from \"@moq/hang\";\nimport type { Effect } from \"@moq/signals\";\nimport type { BufferedRanges } from \"../..\";\nimport type MoqWatch from \"../../element\";\n\nconst MIN_RANGE = 0 as Moq.Time.Milli;\nconst RANGE_STEP = 10 as Moq.Time.Milli;\nconst DEFAULT_MAX = 4000 as Moq.Time.Milli;\nconst LABEL_WIDTH = 48;\n\nfunction drawRanges(\n\tcanvas: HTMLCanvasElement,\n\tranges: BufferedRanges,\n\ttimestamp: Moq.Time.Milli | undefined,\n\tmax: Moq.Time.Milli,\n\tisBuffering: boolean,\n) {\n\tconst ctx = canvas.getContext(\"2d\");\n\tif (!ctx) return;\n\n\tconst dpr = window.devicePixelRatio || 1;\n\tconst rect = canvas.getBoundingClientRect();\n\tconst width = rect.width;\n\tconst height = rect.height;\n\n\tconst canvasW = Math.round(width * dpr);\n\tconst canvasH = Math.round(height * dpr);\n\tif (canvas.width !== canvasW || canvas.height !== canvasH) {\n\t\tcanvas.width = canvasW;\n\t\tcanvas.height = canvasH;\n\t}\n\n\tctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\tctx.clearRect(0, 0, width, height);\n\n\tif (timestamp === undefined) return;\n\n\tconst padding = 2;\n\tconst rangeHeight = height - padding * 2;\n\tconst radius = 2;\n\n\tfor (let i = 0; i < ranges.length; i++) {\n\t\tconst range = ranges[i];\n\t\tconst startMs = (range.start - timestamp) as Moq.Time.Milli;\n\t\tconst endMs = (range.end - timestamp) as Moq.Time.Milli;\n\t\tconst visibleStart = Math.max(0, startMs);\n\t\tconst visibleEnd = Math.min(endMs, max);\n\n\t\tif (visibleEnd <= visibleStart) continue;\n\n\t\tconst x = (visibleStart / max) * width;\n\t\tconst w = Math.max(2, ((visibleEnd - visibleStart) / max) * width);\n\n\t\tctx.globalAlpha = 0.85;\n\t\tctx.fillStyle = isBuffering ? \"#f87171\" : i > 0 ? \"#facc15\" : \"#4ade80\";\n\n\t\tif (typeof ctx.roundRect === \"function\") {\n\t\t\tctx.beginPath();\n\t\t\tctx.roundRect(x, padding, w, rangeHeight, radius);\n\t\t\tctx.fill();\n\t\t} else {\n\t\t\tctx.fillRect(x, padding, w, rangeHeight);\n\t\t}\n\n\t\tif (endMs > max) {\n\t\t\tconst overflowSec = ((endMs - max) / 1000).toFixed(1);\n\t\t\tctx.globalAlpha = 0.7;\n\t\t\tctx.fillStyle = \"black\";\n\t\t\tctx.font = \"500 9px system-ui, sans-serif\";\n\t\t\tctx.textAlign = \"right\";\n\t\t\tctx.textBaseline = \"middle\";\n\t\t\tctx.fillText(`+${overflowSec}s`, x + w - 4, height / 2);\n\t\t}\n\t}\n}\n\nexport function bufferControl(parent: Effect, watch: MoqWatch, max: Moq.Time.Milli = DEFAULT_MAX): HTMLElement {\n\tconst wrapper = document.createElement(\"div\");\n\twrapper.className = \"buffer\";\n\n\tconst viz = document.createElement(\"div\");\n\tviz.className = \"buffer-visualization\";\n\tviz.setAttribute(\"role\", \"slider\");\n\tviz.tabIndex = 0;\n\tviz.setAttribute(\"aria-valuemin\", MIN_RANGE.toString());\n\tviz.setAttribute(\"aria-valuemax\", max.toString());\n\tviz.setAttribute(\"aria-label\", \"Buffer jitter\");\n\n\tconst playhead = document.createElement(\"div\");\n\tplayhead.className = \"buffer-playhead\";\n\n\tconst videoTrack = document.createElement(\"div\");\n\tvideoTrack.className = \"buffer-track buffer-track--video\";\n\tconst videoLabel = document.createElement(\"span\");\n\tvideoLabel.className = \"buffer-track-label\";\n\tvideoLabel.textContent = \"Video\";\n\tconst videoCanvas = document.createElement(\"canvas\");\n\tvideoCanvas.className = \"buffer-canvas\";\n\tvideoTrack.append(videoLabel, videoCanvas);\n\n\tconst audioTrack = document.createElement(\"div\");\n\taudioTrack.className = \"buffer-track buffer-track--audio\";\n\tconst audioLabel = document.createElement(\"span\");\n\taudioLabel.className = \"buffer-track-label\";\n\taudioLabel.textContent = \"Audio\";\n\tconst audioCanvas = document.createElement(\"canvas\");\n\taudioCanvas.className = \"buffer-canvas\";\n\taudioTrack.append(audioLabel, audioCanvas);\n\n\tconst targetArea = document.createElement(\"div\");\n\ttargetArea.className = \"buffer-target-area\";\n\tconst targetLine = document.createElement(\"div\");\n\ttargetLine.className = \"buffer-target-line\";\n\tconst targetLabel = document.createElement(\"span\");\n\ttargetLabel.className = \"buffer-target-label\";\n\ttargetLine.appendChild(targetLabel);\n\ttargetArea.appendChild(targetLine);\n\n\tconst help = document.createElement(\"span\");\n\thelp.className = \"buffer-help\";\n\thelp.textContent = \"click to change latency\";\n\n\tviz.append(playhead, videoTrack, audioTrack, targetArea, help);\n\twrapper.appendChild(viz);\n\n\tlet dragging = false;\n\tlet hasInteracted = false;\n\n\tparent.run((effect) => {\n\t\tconst jitter = effect.get(watch.backend.jitter);\n\t\tconst pct = (jitter / max) * 100;\n\t\ttargetLine.style.left = `${pct}%`;\n\t\ttargetLabel.textContent = `${Math.round(jitter)}ms`;\n\t\tviz.setAttribute(\"aria-valuenow\", jitter.toString());\n\t});\n\n\tconst updateFromX = (clientX: number) => {\n\t\tconst rect = viz.getBoundingClientRect();\n\t\tconst trackWidth = rect.width - LABEL_WIDTH;\n\t\tconst x = Math.max(0, Math.min(clientX - rect.left - LABEL_WIDTH, trackWidth));\n\t\tconst ms = (x / trackWidth) * max;\n\t\tconst snapped = (Math.round(ms / RANGE_STEP) * RANGE_STEP) as Moq.Time.Milli;\n\t\tconst clamped = Math.max(MIN_RANGE, Math.min(max, snapped)) as Moq.Time.Milli;\n\t\twatch.backend.latency.set(clamped);\n\t};\n\n\tconst interact = () => {\n\t\tif (!hasInteracted) {\n\t\t\thasInteracted = true;\n\t\t\thelp.style.display = \"none\";\n\t\t}\n\t};\n\n\tparent.event(viz, \"mousedown\", (e) => {\n\t\tdragging = true;\n\t\tviz.classList.add(\"buffer-visualization--dragging\");\n\t\tinteract();\n\t\tupdateFromX(e.clientX);\n\t});\n\n\tparent.event(document, \"mousemove\", (e) => {\n\t\tif (dragging) updateFromX(e.clientX);\n\t});\n\n\tparent.event(document, \"mouseup\", () => {\n\t\tif (!dragging) return;\n\t\tdragging = false;\n\t\tviz.classList.remove(\"buffer-visualization--dragging\");\n\t});\n\n\tparent.event(viz, \"keydown\", (e) => {\n\t\tlet delta = 0 as Moq.Time.Milli;\n\t\tif (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") {\n\t\t\tdelta = RANGE_STEP;\n\t\t} else if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") {\n\t\t\tdelta = -RANGE_STEP as Moq.Time.Milli;\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t\te.preventDefault();\n\t\tinteract();\n\t\tconst current = watch.backend.jitter.peek();\n\t\tconst value = Math.max(MIN_RANGE, Math.min(max, current + delta)) as Moq.Time.Milli;\n\t\twatch.backend.latency.set(value);\n\t});\n\n\tconst draw = () => {\n\t\tconst timestamp = watch.backend.sync.now();\n\t\tconst isBuffering = watch.backend.video.stalled.peek();\n\t\tdrawRanges(videoCanvas, watch.backend.video.buffered.peek(), timestamp, max, isBuffering);\n\t\tdrawRanges(audioCanvas, watch.backend.audio.buffered.peek(), timestamp, max, isBuffering);\n\t\tparent.animate(draw);\n\t};\n\tparent.animate(draw);\n\n\treturn wrapper;\n}\n","import type { Effect } from \"@moq/signals\";\nimport type MoqWatch from \"../../element\";\n\nexport function bufferingIndicator(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst container = document.createElement(\"div\");\n\tcontainer.className = \"buffering\";\n\tconst spinner = document.createElement(\"div\");\n\tspinner.className = \"buffering-spinner\";\n\tcontainer.appendChild(spinner);\n\n\tparent.run((effect) => {\n\t\tconst buffering = effect.get(watch.backend.video.stalled);\n\t\tconst offline = effect.get(watch.broadcast.status) === \"offline\";\n\t\tcontainer.style.display = buffering && !offline ? \"\" : \"none\";\n\t});\n\n\treturn container;\n}\n","import audio from \"./icons/audio.svg?raw\";\nimport buffer from \"./icons/buffer.svg?raw\";\nimport fullscreenEnter from \"./icons/fullscreen-enter.svg?raw\";\nimport fullscreenExit from \"./icons/fullscreen-exit.svg?raw\";\nimport mute from \"./icons/mute.svg?raw\";\nimport network from \"./icons/network.svg?raw\";\nimport pause from \"./icons/pause.svg?raw\";\nimport play from \"./icons/play.svg?raw\";\nimport stats from \"./icons/stats.svg?raw\";\nimport video from \"./icons/video.svg?raw\";\nimport volumeHigh from \"./icons/volume-high.svg?raw\";\nimport volumeLow from \"./icons/volume-low.svg?raw\";\nimport volumeMedium from \"./icons/volume-medium.svg?raw\";\n\nexport {\n\taudio,\n\tbuffer,\n\tfullscreenEnter,\n\tfullscreenExit,\n\tmute,\n\tnetwork,\n\tpause,\n\tplay,\n\tstats,\n\tvideo,\n\tvolumeHigh,\n\tvolumeLow,\n\tvolumeMedium,\n};\n\nexport function icon(svg: string): HTMLElement {\n\tconst span = document.createElement(\"span\");\n\tspan.className = \"flex-center\";\n\tspan.setAttribute(\"role\", \"img\");\n\tspan.setAttribute(\"aria-hidden\", \"true\");\n\tspan.innerHTML = svg;\n\treturn span;\n}\n","import type { Effect } from \"@moq/signals\";\nimport type MoqWatch from \"../../element\";\nimport { fullscreenEnter, fullscreenExit, icon } from \"../icons\";\n\nexport function fullscreenButton(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst button = document.createElement(\"button\");\n\tbutton.type = \"button\";\n\tbutton.className = \"button flex-center\";\n\tbutton.title = \"Fullscreen\";\n\tbutton.setAttribute(\"aria-label\", \"Fullscreen\");\n\tbutton.replaceChildren(icon(fullscreenEnter));\n\n\tconst updateIcon = () => {\n\t\tconst isFull = !!document.fullscreenElement;\n\t\tbutton.replaceChildren(icon(isFull ? fullscreenExit : fullscreenEnter));\n\t};\n\tparent.event(document, \"fullscreenchange\", updateIcon);\n\n\tparent.event(button, \"click\", () => {\n\t\tif (document.fullscreenElement) {\n\t\t\tdocument.exitFullscreen();\n\t\t} else {\n\t\t\twatch.requestFullscreen();\n\t\t}\n\t});\n\n\treturn button;\n}\n","import type { Effect } from \"@moq/signals\";\nimport type MoqWatch from \"../../element\";\n\nexport function offlineIndicator(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst container = document.createElement(\"div\");\n\tcontainer.className = \"watch-ui__offline-indicator\";\n\tcontainer.setAttribute(\"role\", \"status\");\n\tcontainer.setAttribute(\"aria-live\", \"polite\");\n\n\tconst text = document.createElement(\"span\");\n\ttext.className = \"watch-ui__offline-text\";\n\ttext.textContent = \"OFFLINE\";\n\tcontainer.appendChild(text);\n\n\tparent.run((effect) => {\n\t\tconst offline = effect.get(watch.broadcast.status) === \"offline\";\n\t\tcontainer.style.display = offline ? \"\" : \"none\";\n\t});\n\n\treturn container;\n}\n","import type { Effect } from \"@moq/signals\";\nimport type MoqWatch from \"../../element\";\nimport { icon, pause, play } from \"../icons\";\n\nexport function playPauseButton(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst button = document.createElement(\"button\");\n\tbutton.type = \"button\";\n\tbutton.className = \"button button--playback flex-center\";\n\n\tparent.run((effect) => {\n\t\tconst paused = effect.get(watch.backend.paused);\n\t\tbutton.title = paused ? \"Play\" : \"Pause\";\n\t\tbutton.setAttribute(\"aria-label\", paused ? \"Play\" : \"Pause\");\n\t\tbutton.replaceChildren(icon(paused ? play : pause));\n\t});\n\n\tparent.event(button, \"click\", () => {\n\t\twatch.paused = !watch.paused;\n\t});\n\n\treturn button;\n}\n","import type { Effect } from \"@moq/signals\";\nimport type MoqWatch from \"../../element\";\n\nfunction formatBitrate(bps: number): string {\n\tif (bps >= 1_000_000) return `${(bps / 1_000_000).toFixed(1)} Mbps`;\n\tif (bps >= 1_000) return `${(bps / 1_000).toFixed(0)} kbps`;\n\treturn `${bps} bps`;\n}\n\nexport function qualitySelector(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst wrapper = document.createElement(\"div\");\n\twrapper.className = \"quality-selector\";\n\n\tconst label = document.createElement(\"label\");\n\tlabel.htmlFor = \"moq-watch-quality-select\";\n\tlabel.className = \"quality-label\";\n\tlabel.textContent = \"Quality: \";\n\n\tconst select = document.createElement(\"select\");\n\tselect.id = \"moq-watch-quality-select\";\n\tselect.className = \"quality-select\";\n\n\twrapper.append(label, select);\n\n\tparent.run((effect) => {\n\t\tconst catalog = effect.get(watch.backend.video.source.catalog);\n\t\tconst active = effect.get(watch.backend.video.source.track);\n\t\tconst renditions = catalog?.renditions ?? {};\n\n\t\tselect.replaceChildren();\n\t\tconst auto = document.createElement(\"option\");\n\t\tauto.value = \"\";\n\t\tauto.textContent = \"Auto\";\n\t\tselect.appendChild(auto);\n\n\t\tfor (const [name, config] of Object.entries(renditions)) {\n\t\t\tconst opt = document.createElement(\"option\");\n\t\t\topt.value = name;\n\t\t\tconst dims = config.codedWidth && config.codedHeight ? ` (${config.codedWidth}x${config.codedHeight})` : \"\";\n\t\t\tconst rate = config.bitrate ? ` ${formatBitrate(config.bitrate)}` : \"\";\n\t\t\topt.textContent = `${name}${dims}${rate}`;\n\t\t\tselect.appendChild(opt);\n\t\t}\n\n\t\tselect.value = active ?? \"\";\n\t});\n\n\tparent.event(select, \"change\", () => {\n\t\tconst value = select.value || undefined;\n\t\twatch.backend.video.source.target.update((prev) => ({ ...prev, name: value }));\n\t});\n\n\treturn wrapper;\n}\n","import type { Effect, Signal } from \"@moq/signals\";\nimport { icon, stats } from \"../icons\";\n\nexport function statsButton(parent: Effect, visible: Signal<boolean>): HTMLElement {\n\tconst button = document.createElement(\"button\");\n\tbutton.type = \"button\";\n\tbutton.className = \"button flex-center\";\n\tbutton.replaceChildren(icon(stats));\n\n\tparent.run((effect) => {\n\t\tconst showing = effect.get(visible);\n\t\tbutton.title = showing ? \"Hide stats\" : \"Show stats\";\n\t\tbutton.setAttribute(\"aria-label\", button.title);\n\t});\n\n\tparent.event(button, \"click\", () => {\n\t\tvisible.update((v) => !v);\n\t});\n\n\treturn button;\n}\n","import type { Effect } from \"@moq/signals\";\nimport type MoqWatch from \"../../element\";\nimport { icon, mute, volumeHigh, volumeLow, volumeMedium } from \"../icons\";\n\nfunction volumeIcon(volumePct: number, muted: boolean): string {\n\tif (muted || volumePct === 0) return mute;\n\tif (volumePct <= 33) return volumeLow;\n\tif (volumePct <= 66) return volumeMedium;\n\treturn volumeHigh;\n}\n\nexport function volumeSlider(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst wrapper = document.createElement(\"div\");\n\twrapper.className = \"volume-slider flex-center\";\n\n\tconst button = document.createElement(\"button\");\n\tbutton.type = \"button\";\n\tbutton.className = \"button flex-center\";\n\n\tconst slider = document.createElement(\"input\");\n\tslider.type = \"range\";\n\tslider.min = \"0\";\n\tslider.max = \"100\";\n\n\tconst label = document.createElement(\"span\");\n\tlabel.className = \"volume-label\";\n\n\twrapper.append(button, slider, label);\n\n\tparent.run((effect) => {\n\t\tconst volume = effect.get(watch.backend.audio.volume);\n\t\tconst muted = effect.get(watch.backend.audio.muted);\n\t\tconst pct = Math.round(volume * 100);\n\t\tslider.value = pct.toString();\n\t\tlabel.textContent = pct.toString();\n\t\tbutton.title = muted ? \"Unmute\" : \"Mute\";\n\t\tbutton.setAttribute(\"aria-label\", button.title);\n\t\tbutton.replaceChildren(icon(volumeIcon(pct, muted)));\n\t});\n\n\tparent.event(slider, \"input\", () => {\n\t\twatch.backend.audio.volume.set(Number.parseFloat(slider.value) / 100);\n\t});\n\n\tparent.event(button, \"click\", () => {\n\t\twatch.backend.audio.muted.update((m) => !m);\n\t});\n\n\treturn wrapper;\n}\n","import type { Effect } from \"@moq/signals\";\nimport type MoqWatch from \"../../element\";\n\ntype StatusConfig = { variant: string; text: string };\n\nfunction deriveStatus(\n\turl: URL | undefined,\n\tconnection: \"connecting\" | \"connected\" | \"disconnected\",\n\tbroadcast: \"offline\" | \"loading\" | \"live\",\n): StatusConfig {\n\tif (!url) return { variant: \"error\", text: \"No URL\" };\n\tif (connection === \"disconnected\") return { variant: \"error\", text: \"Disconnected\" };\n\tif (connection === \"connecting\") return { variant: \"connecting\", text: \"Connecting...\" };\n\tif (broadcast === \"offline\") return { variant: \"error\", text: \"Offline\" };\n\tif (broadcast === \"loading\") return { variant: \"loading\", text: \"Loading...\" };\n\tif (broadcast === \"live\") return { variant: \"live\", text: \"Live\" };\n\treturn { variant: \"connected\", text: \"Connected\" };\n}\n\nexport function watchStatusIndicator(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst wrapper = document.createElement(\"div\");\n\twrapper.className = \"status-indicator flex-center\";\n\n\tconst dot = document.createElement(\"span\");\n\tconst text = document.createElement(\"span\");\n\twrapper.append(dot, text);\n\n\tparent.run((effect) => {\n\t\tconst url = effect.get(watch.connection.url);\n\t\tconst conn = effect.get(watch.connection.status);\n\t\tconst broadcast = effect.get(watch.broadcast.status);\n\t\tconst { variant, text: label } = deriveStatus(url, conn, broadcast);\n\n\t\tdot.className = `status-indicator-dot status-indicator-dot--${variant}`;\n\t\ttext.className = `status-indicator-text status-indicator-text--${variant}`;\n\t\ttext.textContent = label;\n\t});\n\n\treturn wrapper;\n}\n","import type { Effect, Signal } from \"@moq/signals\";\nimport * as DOM from \"@moq/signals/dom\";\nimport type MoqWatch from \"../element\";\nimport { audio, buffer, icon, network, video } from \"./icons\";\n\nconst POLL_MS = 250;\n\ntype Kind = \"network\" | \"video\" | \"audio\" | \"buffer\";\n\nfunction row(kind: Kind, label: string, svg: string): { el: HTMLElement; data: HTMLSpanElement } {\n\tconst el = DOM.create(\"div\", { className: `stats-item stats-item--${kind}` });\n\n\tconst iconWrap = DOM.create(\"div\", { className: \"stats-icon-wrapper\" });\n\ticonWrap.appendChild(icon(svg));\n\n\tconst title = DOM.create(\"span\", { className: \"stats-item-title\" }, label);\n\tconst data = DOM.create(\"span\", { className: \"stats-item-data\" }, \"N/A\");\n\n\tconst detail = DOM.create(\"div\", { className: \"stats-item-detail\" });\n\tdetail.append(title, data);\n\n\tel.append(iconWrap, detail);\n\treturn { el, data };\n}\n\nfunction formatBitrate(bps: number): string {\n\tif (bps >= 1_000_000) return `${(bps / 1_000_000).toFixed(1)}Mbps`;\n\tif (bps >= 1_000) return `${(bps / 1_000).toFixed(0)}kbps`;\n\treturn `${bps.toFixed(0)}bps`;\n}\n\nfunction formatBandwidth(bps: number | undefined, dir: \"up\" | \"down\"): string | null {\n\tif (bps === undefined || bps <= 0) return null;\n\tconst arrow = dir === \"down\" ? \"↓\" : \"↑\";\n\tif (bps >= 1_000_000_000) return `${arrow} ${(bps / 1_000_000_000).toFixed(1)}Gbps`;\n\treturn `${arrow} ${formatBitrate(bps)}`;\n}\n\nfunction networkRow(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst { el, data } = row(\"network\", \"network\", network);\n\n\tparent.interval(() => {\n\t\tconst conn = watch.connection.established.peek();\n\t\tif (!conn) {\n\t\t\tdata.textContent = \"N/A\";\n\t\t\treturn;\n\t\t}\n\t\tconst rtt = conn.rtt?.peek();\n\t\tconst parts = [\n\t\t\tformatBandwidth(conn.recvBandwidth?.peek(), \"down\"),\n\t\t\tformatBandwidth(conn.sendBandwidth?.peek(), \"up\"),\n\t\t\trtt !== undefined && rtt > 0 ? `${rtt.toFixed(0)}ms` : null,\n\t\t].filter((p): p is string => p !== null);\n\t\tdata.textContent = parts.length > 0 ? parts.join(\"\\n\") : \"N/A\";\n\t}, POLL_MS);\n\n\treturn el;\n}\n\nfunction videoRow(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst { el, data } = row(\"video\", \"video\", video);\n\tlet prevFrames = 0;\n\tlet prevBytes = 0;\n\tlet prevWhen = performance.now();\n\n\tparent.interval(() => {\n\t\tconst catalog = watch.backend.video.source.catalog.peek();\n\t\tconst stats = watch.backend.video.stats.peek();\n\t\tconst now = performance.now();\n\t\tconst elapsedMs = now - prevWhen;\n\n\t\tlet fps: number | undefined;\n\t\tif (stats && prevFrames > 0 && elapsedMs > 0) {\n\t\t\tconst delta = stats.frameCount - prevFrames;\n\t\t\tif (delta > 0) fps = delta / (elapsedMs / 1000);\n\t\t}\n\n\t\tlet bitrate: string | undefined;\n\t\tif (stats && prevBytes > 0 && elapsedMs > 0) {\n\t\t\tconst delta = stats.bytesReceived - prevBytes;\n\t\t\tif (delta > 0) bitrate = formatBitrate(delta * 8 * (1000 / elapsedMs));\n\t\t}\n\n\t\tif (stats) {\n\t\t\tprevFrames = stats.frameCount;\n\t\t\tprevBytes = stats.bytesReceived;\n\t\t\tprevWhen = now;\n\t\t}\n\n\t\tconst { width, height } = catalog?.display ?? {};\n\t\tdata.textContent = [\n\t\t\twidth && height ? `${width}x${height}` : \"N/A\",\n\t\t\tfps !== undefined ? `@${fps.toFixed(1)} fps` : \"N/A\",\n\t\t\tbitrate ?? \"N/A\",\n\t\t].join(\"\\n\");\n\t}, POLL_MS);\n\n\treturn el;\n}\n\nfunction audioRow(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst { el, data } = row(\"audio\", \"audio\", audio);\n\tlet prevBytes = 0;\n\tlet prevWhen = performance.now();\n\n\tparent.interval(() => {\n\t\tconst track = watch.backend.audio.source.track.peek();\n\t\tconst config = watch.backend.audio.source.config.peek();\n\t\tconst stats = watch.backend.audio.stats.peek();\n\n\t\tif (!track || !config) {\n\t\t\tdata.textContent = \"N/A\";\n\t\t\treturn;\n\t\t}\n\n\t\tconst now = performance.now();\n\t\tlet bitrate: string | undefined;\n\t\tif (stats && prevBytes > 0) {\n\t\t\tconst delta = stats.bytesReceived - prevBytes;\n\t\t\tconst elapsedMs = now - prevWhen;\n\t\t\tif (delta > 0 && elapsedMs > 0) bitrate = formatBitrate(delta * 8 * (1000 / elapsedMs));\n\t\t}\n\n\t\tif (stats) {\n\t\t\tprevBytes = stats.bytesReceived;\n\t\t\tprevWhen = now;\n\t\t}\n\n\t\tconst parts: string[] = [];\n\t\tif (config.sampleRate) parts.push(`${(config.sampleRate / 1000).toFixed(1)}kHz`);\n\t\tif (config.numberOfChannels) parts.push(`${config.numberOfChannels}ch`);\n\t\tparts.push(bitrate ?? \"N/A\");\n\t\tif (config.codec) parts.push(config.codec);\n\t\tdata.textContent = parts.length > 0 ? parts.join(\"\\n\") : \"N/A\";\n\t}, POLL_MS);\n\n\treturn el;\n}\n\nfunction bufferRow(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst { el, data } = row(\"buffer\", \"buffer\", buffer);\n\n\tparent.run((effect) => {\n\t\tconst jitter = effect.get(watch.backend.jitter);\n\t\tdata.textContent = `${Math.round(jitter)}ms`;\n\t});\n\n\treturn el;\n}\n\nexport function statsPanel(parent: Effect, watch: MoqWatch, visible: Signal<boolean>): HTMLElement {\n\tconst wrap = DOM.create(\"div\", { className: \"stats\" });\n\tconst panel = DOM.create(\"div\", { className: \"stats-panel\" });\n\twrap.appendChild(panel);\n\n\tparent.run((effect) => {\n\t\tconst showing = effect.get(visible);\n\t\twrap.style.display = showing ? \"\" : \"none\";\n\t\tif (!showing) return;\n\n\t\tDOM.render(effect, panel, networkRow(effect, watch));\n\t\tDOM.render(effect, panel, videoRow(effect, watch));\n\t\tDOM.render(effect, panel, audioRow(effect, watch));\n\t\tDOM.render(effect, panel, bufferRow(effect, watch));\n\t});\n\n\treturn wrap;\n}\n","@import \"./base.css\";\n@import \"./stats.css\";\n@import \"./controls.css\";\n@import \"./volume-slider.css\";\n@import \"./quality-selector.css\";\n@import \"./buffer-control.css\";\n@import \"./buffering-indicator.css\";\n@import \"./offline-indicator.css\";\n@import \"./status-indicator.css\";\n","import { Effect, Signal } from \"@moq/signals\";\nimport * as DOM from \"@moq/signals/dom\";\nimport type MoqWatch from \"../element\";\nimport { bufferControl } from \"./components/buffer-control\";\nimport { bufferingIndicator } from \"./components/buffering-indicator\";\nimport { fullscreenButton } from \"./components/fullscreen-button\";\nimport { offlineIndicator } from \"./components/offline-indicator\";\nimport { playPauseButton } from \"./components/play-pause\";\nimport { qualitySelector } from \"./components/quality-selector\";\nimport { statsButton } from \"./components/stats-button\";\nimport { volumeSlider } from \"./components/volume-slider\";\nimport { watchStatusIndicator } from \"./components/watch-status-indicator\";\nimport { statsPanel } from \"./stats\";\nimport styles from \"./styles/index.css?inline\";\n\nexport default class MoqWatchUi extends HTMLElement {\n\t#signals?: Effect;\n\t#root: ShadowRoot;\n\t#watch = new Signal<MoqWatch | undefined>(undefined);\n\t#observer: MutationObserver;\n\n\tconstructor() {\n\t\tsuper();\n\t\tthis.#root = this.attachShadow({ mode: \"open\" });\n\n\t\tconst style = document.createElement(\"style\");\n\t\tstyle.textContent = styles;\n\t\tthis.#root.appendChild(style);\n\n\t\tthis.#observer = new MutationObserver(() => this.#updateWatch());\n\t}\n\n\tconnectedCallback() {\n\t\tthis.#updateWatch();\n\t\tthis.#observer.observe(this, { childList: true });\n\n\t\tconst signals = new Effect();\n\t\tthis.#signals = signals;\n\t\tsignals.run(this.#render.bind(this));\n\t}\n\n\tdisconnectedCallback() {\n\t\tthis.#observer.disconnect();\n\t\tthis.#signals?.close();\n\t\tthis.#signals = undefined;\n\t}\n\n\t#updateWatch() {\n\t\tconst watch = this.querySelector(\"moq-watch\") as MoqWatch | null;\n\t\tthis.#watch.set(watch ?? undefined);\n\t}\n\n\t#render(effect: Effect) {\n\t\tconst watch = effect.get(this.#watch);\n\t\tif (!watch) return;\n\n\t\tconst visible = new Signal(false);\n\n\t\tconst videoContainer = DOM.create(\"div\", { className: \"video-container\" });\n\t\tvideoContainer.append(\n\t\t\tDOM.create(\"slot\"),\n\t\t\tstatsPanel(effect, watch, visible),\n\t\t\tbufferingIndicator(effect, watch),\n\t\t\tofflineIndicator(effect, watch),\n\t\t);\n\n\t\tconst controls = DOM.create(\"div\", { className: \"controls\" });\n\n\t\tconst playback = DOM.create(\"div\", { className: \"playback-controls flex-align-center\" });\n\t\tplayback.append(\n\t\t\tplayPauseButton(effect, watch),\n\t\t\tvolumeSlider(effect, watch),\n\t\t\twatchStatusIndicator(effect, watch),\n\t\t\tstatsButton(effect, visible),\n\t\t\tfullscreenButton(effect, watch),\n\t\t);\n\n\t\tconst latency = DOM.create(\"div\", { className: \"latency-controls\" });\n\t\tlatency.append(bufferControl(effect, watch), qualitySelector(effect, watch));\n\n\t\tcontrols.append(playback, latency);\n\n\t\tDOM.render(effect, this.#root, videoContainer);\n\t\tDOM.render(effect, this.#root, controls);\n\t}\n}\n\ncustomElements.define(\"moq-watch-ui\", MoqWatchUi);\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t\"moq-watch-ui\": MoqWatchUi;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"element.js","names":["#root","#observer","#updateWatch","#signals","#render","#watch","#wireChrome"],"sources":["../../src/ui/components/buffering-indicator.ts","../../src/ui/icons.ts","../../src/ui/components/center-play.ts","../../src/ui/fullscreen.ts","../../src/ui/components/button.ts","../../src/ui/components/fullscreen-button.ts","../../src/ui/format.ts","../../src/ui/components/live-badge.ts","../../src/ui/components/play-pause.ts","../../src/ui/components/settings-button.ts","../../src/ui/components/volume-slider.ts","../../src/ui/components/control-bar.ts","../../src/ui/components/offline-indicator.ts","../../src/ui/graph.ts","../../src/ui/stats.ts","../../src/ui/components/buffer-control.ts","../../src/ui/components/latency.ts","../../src/ui/components/quality.ts","../../src/ui/components/settings-panel.ts","../../src/ui/styles/index.css?inline","../../src/ui/element.ts"],"sourcesContent":["import type { Effect } from \"@moq/signals\";\nimport type MoqWatch from \"../../element\";\n\nexport function bufferingIndicator(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst container = document.createElement(\"div\");\n\tcontainer.className = \"buffering\";\n\tconst spinner = document.createElement(\"div\");\n\tspinner.className = \"buffering-spinner\";\n\tcontainer.appendChild(spinner);\n\n\tparent.run((effect) => {\n\t\tconst buffering = effect.get(watch.backend.video.stalled);\n\t\tconst offline = effect.get(watch.broadcast.status) === \"offline\";\n\t\tcontainer.style.display = buffering && !offline ? \"\" : \"none\";\n\t});\n\n\treturn container;\n}\n","import audio from \"./icons/audio.svg?raw\";\nimport buffer from \"./icons/buffer.svg?raw\";\nimport check from \"./icons/check.svg?raw\";\nimport close from \"./icons/close.svg?raw\";\nimport fullscreenEnter from \"./icons/fullscreen-enter.svg?raw\";\nimport fullscreenExit from \"./icons/fullscreen-exit.svg?raw\";\nimport gauge from \"./icons/gauge.svg?raw\";\nimport mute from \"./icons/mute.svg?raw\";\nimport network from \"./icons/network.svg?raw\";\nimport pause from \"./icons/pause.svg?raw\";\nimport play from \"./icons/play.svg?raw\";\nimport settings from \"./icons/settings.svg?raw\";\nimport stats from \"./icons/stats.svg?raw\";\nimport video from \"./icons/video.svg?raw\";\nimport volumeHigh from \"./icons/volume-high.svg?raw\";\nimport volumeLow from \"./icons/volume-low.svg?raw\";\nimport volumeMedium from \"./icons/volume-medium.svg?raw\";\n\nexport {\n\taudio,\n\tbuffer,\n\tcheck,\n\tclose,\n\tfullscreenEnter,\n\tfullscreenExit,\n\tgauge,\n\tmute,\n\tnetwork,\n\tpause,\n\tplay,\n\tsettings,\n\tstats,\n\tvideo,\n\tvolumeHigh,\n\tvolumeLow,\n\tvolumeMedium,\n};\n\nexport function icon(svg: string): HTMLElement {\n\tconst span = document.createElement(\"span\");\n\tspan.className = \"flex-center\";\n\tspan.setAttribute(\"role\", \"img\");\n\tspan.setAttribute(\"aria-hidden\", \"true\");\n\tspan.innerHTML = svg;\n\treturn span;\n}\n","import type { Effect } from \"@moq/signals\";\nimport type MoqWatch from \"../../element\";\nimport { icon, play } from \"../icons\";\n\n/** A large centered play button shown while paused. */\nexport function centerPlay(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst button = document.createElement(\"button\");\n\tbutton.type = \"button\";\n\tbutton.className = \"center-play flex-center\";\n\tbutton.title = \"Play\";\n\tbutton.setAttribute(\"aria-label\", \"Play\");\n\tbutton.replaceChildren(icon(play));\n\n\tparent.run((effect) => {\n\t\tconst paused = effect.get(watch.backend.paused);\n\t\tbutton.style.display = paused ? \"\" : \"none\";\n\t});\n\n\tparent.event(button, \"click\", () => {\n\t\twatch.paused = false;\n\t});\n\n\treturn button;\n}\n","// Cross-browser fullscreen for the player.\n//\n// We fullscreen the shadow `.player` container (not the bare <moq-watch>) so the\n// overlay chrome stays visible. Safari needs webkit-prefixed methods, and iPhone\n// has no element fullscreen API at all: there we fall back to the native <video>\n// fullscreen if an MSE element exists, otherwise a CSS pseudo-fullscreen that\n// pins the player to the viewport (the only option for the canvas backend).\nimport type { Effect } from \"@moq/signals\";\n\ntype WebkitDocument = Document & {\n\twebkitFullscreenElement?: Element | null;\n\twebkitExitFullscreen?: () => void;\n};\n\ntype WebkitElement = HTMLElement & {\n\twebkitRequestFullscreen?: () => void;\n};\n\n// iOS exposes fullscreen only on the media element itself, with its own\n// begin/end events and a `webkitDisplayingFullscreen` state flag.\ntype IosVideo = HTMLVideoElement & {\n\twebkitEnterFullscreen?: () => void;\n\twebkitSupportsFullscreen?: boolean;\n\twebkitDisplayingFullscreen?: boolean;\n};\n\nconst PSEUDO_CLASS = \"player--pseudo-fullscreen\";\n\nexport interface Fullscreen {\n\t/** Whether the player is currently maximized (real or pseudo fullscreen). */\n\tactive(): boolean;\n\t/** Enter if windowed, exit if fullscreen. Must run inside a user gesture. */\n\ttoggle(): void;\n\t/** Subscribe to state changes; returns an unsubscribe function. */\n\tonChange(fn: () => void): () => void;\n}\n\n/**\n * @param parent Effect that owns the document listeners (auto-removed on cleanup).\n * @param player The shadow container to maximize.\n * @param media Resolves the current <canvas>/<video>, used for the iOS path.\n */\nexport function createFullscreen(\n\tparent: Effect,\n\tplayer: HTMLElement,\n\tmedia: () => HTMLElement | undefined,\n): Fullscreen {\n\tconst doc = document as WebkitDocument;\n\tconst listeners = new Set<() => void>();\n\tconst notify = () => {\n\t\tfor (const fn of listeners) fn();\n\t};\n\n\t// Real fullscreen changes (incl. Esc / browser chrome) flow through notify too.\n\tparent.event(document, \"fullscreenchange\", notify);\n\tparent.event(document, \"webkitfullscreenchange\", notify);\n\n\t// iPhone native <video> fullscreen doesn't fire document events, so we track\n\t// the element directly via its begin/end events + webkitDisplayingFullscreen.\n\tlet iosVideo: IosVideo | undefined;\n\n\tconst realActive = () =>\n\t\t!!(document.fullscreenElement || doc.webkitFullscreenElement || iosVideo?.webkitDisplayingFullscreen);\n\tconst pseudoActive = () => player.classList.contains(PSEUDO_CLASS);\n\tconst active = () => realActive() || pseudoActive();\n\n\tconst enter = () => {\n\t\tconst el = player as WebkitElement;\n\t\tif (el.requestFullscreen) {\n\t\t\t// Promise may reject (denied / not a user gesture): fall back gracefully.\n\t\t\tel.requestFullscreen().catch(() => enterPseudo());\n\t\t\treturn;\n\t\t}\n\t\tif (el.webkitRequestFullscreen) {\n\t\t\tel.webkitRequestFullscreen();\n\t\t\treturn;\n\t\t}\n\n\t\t// iPhone: no element fullscreen. Use native video fullscreen if available.\n\t\tconst video = media() as IosVideo | undefined;\n\t\tif (video?.webkitEnterFullscreen && video.webkitSupportsFullscreen !== false) {\n\t\t\t// Bind the element's fullscreen events once so active()/icon stay in sync.\n\t\t\tif (iosVideo !== video) {\n\t\t\t\tiosVideo = video;\n\t\t\t\tparent.event(video, \"webkitbeginfullscreen\", notify);\n\t\t\t\tparent.event(video, \"webkitendfullscreen\", notify);\n\t\t\t}\n\t\t\tvideo.webkitEnterFullscreen();\n\t\t\treturn;\n\t\t}\n\n\t\tenterPseudo();\n\t};\n\n\tconst exit = () => {\n\t\tif (pseudoActive()) {\n\t\t\texitPseudo();\n\t\t\treturn;\n\t\t}\n\t\tif (document.exitFullscreen) {\n\t\t\tdocument.exitFullscreen().catch(() => {});\n\t\t\treturn;\n\t\t}\n\t\tdoc.webkitExitFullscreen?.();\n\t};\n\n\tconst enterPseudo = () => {\n\t\tplayer.classList.add(PSEUDO_CLASS);\n\t\tnotify();\n\t};\n\tconst exitPseudo = () => {\n\t\tplayer.classList.remove(PSEUDO_CLASS);\n\t\tnotify();\n\t};\n\n\tconst toggle = () => (active() ? exit() : enter());\n\n\tconst onChange = (fn: () => void): (() => void) => {\n\t\tlisteners.add(fn);\n\t\treturn () => listeners.delete(fn);\n\t};\n\n\treturn { active, toggle, onChange };\n}\n","import { icon } from \"../icons\";\n\n/** A round overlay control button with an SVG glyph. */\nexport function controlButton(svg: string, label: string): HTMLButtonElement {\n\tconst button = document.createElement(\"button\");\n\tbutton.type = \"button\";\n\tbutton.className = \"control flex-center\";\n\tbutton.title = label;\n\tbutton.setAttribute(\"aria-label\", label);\n\tbutton.replaceChildren(icon(svg));\n\treturn button;\n}\n","import type { Effect } from \"@moq/signals\";\nimport type MoqWatch from \"../../element\";\nimport { createFullscreen } from \"../fullscreen\";\nimport { fullscreenEnter, fullscreenExit, icon } from \"../icons\";\nimport { controlButton } from \"./button\";\n\n/** Fullscreen toggle button wired to the shared cross-browser fullscreen controller. */\nexport function fullscreenButton(parent: Effect, player: HTMLElement, watch: MoqWatch): HTMLElement {\n\tconst button = controlButton(fullscreenEnter, \"Fullscreen\");\n\n\t// The MSE backend renders into a <video>; the WebCodecs backend into a <canvas>.\n\tconst media = () => (watch.querySelector(\"video\") ?? watch.querySelector(\"canvas\")) as HTMLElement | undefined;\n\tconst fullscreen = createFullscreen(parent, player, media);\n\n\tconst updateIcon = () => {\n\t\tconst isFull = fullscreen.active();\n\t\tbutton.replaceChildren(icon(isFull ? fullscreenExit : fullscreenEnter));\n\t\tbutton.title = isFull ? \"Exit fullscreen\" : \"Fullscreen\";\n\t\tbutton.setAttribute(\"aria-label\", button.title);\n\t};\n\tupdateIcon();\n\tparent.cleanup(fullscreen.onChange(updateIcon));\n\n\tparent.event(button, \"click\", () => fullscreen.toggle());\n\n\treturn button;\n}\n","// Shared human-readable formatters for the watch UI.\n\n/** Format a bitrate (bits per second) as Mbps/kbps/bps. */\nexport function formatBitrate(bps: number): string {\n\tif (bps >= 1_000_000) return `${(bps / 1_000_000).toFixed(1)} Mbps`;\n\tif (bps >= 1_000) return `${(bps / 1_000).toFixed(0)} kbps`;\n\treturn `${Math.round(bps)} bps`;\n}\n\n/** Format a bandwidth estimate with a direction arrow, or null if unavailable. */\nexport function formatBandwidth(bps: number | undefined, dir: \"up\" | \"down\"): string | null {\n\tif (bps === undefined || bps <= 0) return null;\n\tconst arrow = dir === \"down\" ? \"↓\" : \"↑\";\n\tif (bps >= 1_000_000_000) return `${arrow} ${(bps / 1_000_000_000).toFixed(1)} Gbps`;\n\treturn `${arrow} ${formatBitrate(bps)}`;\n}\n\n/** Format a duration in milliseconds, switching to seconds past 1s. */\nexport function formatMillis(ms: number): string {\n\tif (ms < 1000) return `${Math.round(ms)} ms`;\n\treturn `${(ms / 1000).toFixed(2)} s`;\n}\n\n/** Format an audio sample rate (Hz) as kHz. */\nexport function formatHz(hz: number): string {\n\treturn `${(hz / 1000).toFixed(1)} kHz`;\n}\n\n/** Format a frame rate. */\nexport function formatFps(fps: number): string {\n\treturn `${fps.toFixed(1)} fps`;\n}\n","import type { Effect } from \"@moq/signals\";\nimport type MoqWatch from \"../../element\";\nimport { formatMillis } from \"../format\";\nimport type { UiState } from \"../state\";\n\ntype Variant = \"live\" | \"loading\" | \"connecting\" | \"error\";\n\nfunction deriveStatus(\n\turl: URL | undefined,\n\tconnection: \"connecting\" | \"connected\" | \"disconnected\",\n\tbroadcast: \"offline\" | \"loading\" | \"live\",\n): { variant: Variant; text: string } {\n\tif (!url) return { variant: \"error\", text: \"No URL\" };\n\tif (connection === \"disconnected\") return { variant: \"error\", text: \"Disconnected\" };\n\tif (connection === \"connecting\") return { variant: \"connecting\", text: \"Connecting\" };\n\tif (broadcast === \"offline\") return { variant: \"error\", text: \"Offline\" };\n\tif (broadcast === \"loading\") return { variant: \"loading\", text: \"Loading\" };\n\tif (broadcast === \"live\") return { variant: \"live\", text: \"Live\" };\n\treturn { variant: \"loading\", text: \"Connected\" };\n}\n\n/**\n * Compact status pill. Shows a pulsing dot + LIVE when streaming, plus the\n * current buffer latency. Clicking jumps to the Latency tab.\n */\nexport function liveBadge(parent: Effect, watch: MoqWatch, state: UiState): HTMLElement {\n\tconst button = document.createElement(\"button\");\n\tbutton.type = \"button\";\n\tbutton.className = \"badge flex-align-center\";\n\tbutton.title = \"Latency settings\";\n\n\tconst dot = document.createElement(\"span\");\n\tdot.className = \"badge-dot\";\n\tconst text = document.createElement(\"span\");\n\ttext.className = \"badge-text\";\n\tconst latency = document.createElement(\"span\");\n\tlatency.className = \"badge-latency\";\n\n\tbutton.append(dot, text, latency);\n\n\tparent.run((effect) => {\n\t\tconst url = effect.get(watch.connection.url);\n\t\tconst conn = effect.get(watch.connection.status);\n\t\tconst broadcast = effect.get(watch.broadcast.status);\n\t\tconst { variant, text: label } = deriveStatus(url, conn, broadcast);\n\n\t\t// The center overlay already shows a prominent OFFLINE notice; don't\n\t\t// duplicate it in the bar.\n\t\tbutton.style.display = broadcast === \"offline\" ? \"none\" : \"\";\n\t\tbutton.dataset.variant = variant;\n\t\ttext.textContent = label.toUpperCase();\n\t});\n\n\tparent.run((effect) => {\n\t\t// Show the total added latency (jitter buffer + codec frame overhead),\n\t\t// which is what the viewer actually experiences.\n\t\tconst total = effect.get(watch.backend.sync.buffer);\n\t\tlatency.textContent = `+${formatMillis(total)} latency`;\n\t});\n\n\tparent.event(button, \"click\", () => {\n\t\tstate.tab.set(\"latency\");\n\t\tstate.panel.set(true);\n\t});\n\n\treturn button;\n}\n","import type { Effect } from \"@moq/signals\";\nimport type MoqWatch from \"../../element\";\nimport { icon, pause, play } from \"../icons\";\nimport { controlButton } from \"./button\";\n\n/** Play/pause control bound to the backend paused state. */\nexport function playPauseButton(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst button = controlButton(play, \"Play\");\n\n\tparent.run((effect) => {\n\t\tconst paused = effect.get(watch.backend.paused);\n\t\tbutton.title = paused ? \"Play\" : \"Pause\";\n\t\tbutton.setAttribute(\"aria-label\", button.title);\n\t\tbutton.replaceChildren(icon(paused ? play : pause));\n\t});\n\n\tparent.event(button, \"click\", () => {\n\t\twatch.paused = !watch.paused;\n\t});\n\n\treturn button;\n}\n","import type { Effect } from \"@moq/signals\";\nimport { settings } from \"../icons\";\nimport type { UiState } from \"../state\";\nimport { controlButton } from \"./button\";\n\n/** Gear button that toggles the settings panel open/closed. */\nexport function settingsButton(parent: Effect, state: UiState): HTMLElement {\n\tconst button = controlButton(settings, \"Settings\");\n\n\tparent.run((effect) => {\n\t\tconst open = effect.get(state.panel);\n\t\tbutton.classList.toggle(\"control--active\", open);\n\t\tbutton.title = open ? \"Close settings\" : \"Settings\";\n\t\tbutton.setAttribute(\"aria-label\", button.title);\n\t\tbutton.setAttribute(\"aria-expanded\", String(open));\n\t});\n\n\tparent.event(button, \"click\", () => {\n\t\tstate.panel.update((v) => !v);\n\t});\n\n\treturn button;\n}\n","import type { Effect } from \"@moq/signals\";\nimport type MoqWatch from \"../../element\";\nimport { icon, mute, volumeHigh, volumeLow, volumeMedium } from \"../icons\";\n\nfunction volumeIcon(volumePct: number, muted: boolean): string {\n\tif (muted || volumePct === 0) return mute;\n\tif (volumePct <= 33) return volumeLow;\n\tif (volumePct <= 66) return volumeMedium;\n\treturn volumeHigh;\n}\n\n/** Volume control with a mute toggle; the slider expands on hover. */\nexport function volumeSlider(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst wrapper = document.createElement(\"div\");\n\twrapper.className = \"volume flex-align-center\";\n\n\tconst button = document.createElement(\"button\");\n\tbutton.type = \"button\";\n\tbutton.className = \"control flex-center\";\n\n\tconst slider = document.createElement(\"input\");\n\tslider.type = \"range\";\n\tslider.className = \"volume-track\";\n\tslider.min = \"0\";\n\tslider.max = \"100\";\n\tslider.setAttribute(\"aria-label\", \"Volume\");\n\n\twrapper.append(button, slider);\n\n\tparent.run((effect) => {\n\t\tconst volume = effect.get(watch.backend.audio.volume);\n\t\tconst muted = effect.get(watch.backend.audio.muted);\n\t\tconst pct = Math.round(volume * 100);\n\t\tconst shown = muted ? 0 : pct;\n\t\tslider.value = shown.toString();\n\t\t// Drive the filled portion of the track via a CSS variable.\n\t\tslider.style.setProperty(\"--fill\", `${shown}%`);\n\t\tbutton.title = muted ? \"Unmute\" : \"Mute\";\n\t\tbutton.setAttribute(\"aria-label\", button.title);\n\t\tbutton.replaceChildren(icon(volumeIcon(pct, muted)));\n\t});\n\n\tparent.event(slider, \"input\", () => {\n\t\tconst value = Number.parseFloat(slider.value) / 100;\n\t\twatch.backend.audio.volume.set(value);\n\t\t// Any non-zero adjustment implies an intent to hear audio.\n\t\tif (value > 0) watch.backend.audio.muted.set(false);\n\t});\n\n\tparent.event(button, \"click\", () => {\n\t\twatch.backend.audio.muted.update((m) => !m);\n\t});\n\n\treturn wrapper;\n}\n","import type { Effect } from \"@moq/signals\";\nimport * as DOM from \"@moq/signals/dom\";\nimport type MoqWatch from \"../../element\";\nimport type { UiState } from \"../state\";\nimport { fullscreenButton } from \"./fullscreen-button\";\nimport { liveBadge } from \"./live-badge\";\nimport { playPauseButton } from \"./play-pause\";\nimport { settingsButton } from \"./settings-button\";\nimport { volumeSlider } from \"./volume-slider\";\n\n/** The bottom control bar: essentials on the left, settings + fullscreen on the right. */\nexport function controlBar(parent: Effect, watch: MoqWatch, state: UiState, player: HTMLElement): HTMLElement {\n\tconst bar = DOM.create(\"div\", { className: \"controls\" });\n\n\tconst left = DOM.create(\"div\", { className: \"controls-group\" });\n\tleft.append(playPauseButton(parent, watch), volumeSlider(parent, watch), liveBadge(parent, watch, state));\n\n\tconst spacer = DOM.create(\"div\", { className: \"controls-spacer\" });\n\n\tconst right = DOM.create(\"div\", { className: \"controls-group\" });\n\tright.append(settingsButton(parent, state), fullscreenButton(parent, player, watch));\n\n\tbar.append(left, spacer, right);\n\treturn bar;\n}\n","import type { Effect } from \"@moq/signals\";\nimport type MoqWatch from \"../../element\";\n\nexport function offlineIndicator(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst container = document.createElement(\"div\");\n\tcontainer.className = \"watch-ui__offline-indicator\";\n\tcontainer.setAttribute(\"role\", \"status\");\n\tcontainer.setAttribute(\"aria-live\", \"polite\");\n\n\tconst text = document.createElement(\"span\");\n\ttext.className = \"watch-ui__offline-text\";\n\ttext.textContent = \"OFFLINE\";\n\tcontainer.appendChild(text);\n\n\tparent.run((effect) => {\n\t\tconst offline = effect.get(watch.broadcast.status) === \"offline\";\n\t\tcontainer.style.display = offline ? \"\" : \"none\";\n\t});\n\n\treturn container;\n}\n","import type { Effect } from \"@moq/signals\";\n\nexport interface GraphOptions {\n\t// Fixed y-axis maximum. If omitted, the graph autoscales to its rolling peak.\n\tmax?: number;\n\t// How many samples of history to retain (older samples scroll off the left).\n\tsamples?: number;\n\t// Stroke/fill color for the line (any CSS color).\n\tcolor?: string;\n\t// Formats the latest value for the readout in the corner.\n\tformat?: (v: number) => string;\n}\n\nexport interface Graph {\n\tel: HTMLElement;\n\t// Append a sample. Pass undefined to record a gap (drawn as zero).\n\tpush(value: number | undefined): void;\n}\n\nconst DEFAULT_SAMPLES = 120;\n\n/** Normalize any CSS color and apply an alpha, so the gradient works for named/rgb/hsl inputs too. */\nfunction withAlpha(color: string, alpha: number): string {\n\tconst ctx = document.createElement(\"canvas\").getContext(\"2d\");\n\tif (!ctx) return color;\n\tctx.fillStyle = color;\n\tconst normalized = ctx.fillStyle; // canvas returns \"#rrggbb\" or \"rgba(...)\"\n\tif (normalized.startsWith(\"#\")) {\n\t\tconst n = Number.parseInt(normalized.slice(1), 16);\n\t\treturn `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;\n\t}\n\tconst parts = normalized.match(/[\\d.]+/g);\n\tif (parts && parts.length >= 3) return `rgba(${parts[0]}, ${parts[1]}, ${parts[2]}, ${alpha})`;\n\treturn color;\n}\n\n/**\n * A rolling time-series sparkline. Samples scroll right-to-left and the area\n * under the line is filled with a fading gradient. Redraws are event-driven:\n * each `push` and any canvas resize triggers a repaint (no animation loop).\n */\nexport function graph(parent: Effect, title: string, opts?: GraphOptions): Graph {\n\tconst color = opts?.color ?? \"#4ade80\";\n\t// Precompute the gradient stops once (the color is fixed) instead of per frame.\n\tconst fillTop = withAlpha(color, 0.33);\n\tconst fillBottom = withAlpha(color, 0);\n\t// Clamp to a sane positive integer so a bad `samples` can't wedge the trim loop.\n\tconst capacity = Number.isFinite(opts?.samples)\n\t\t? Math.max(1, Math.floor(opts?.samples as number))\n\t\t: DEFAULT_SAMPLES;\n\n\tconst el = document.createElement(\"div\");\n\tel.className = \"graph\";\n\n\tconst header = document.createElement(\"div\");\n\theader.className = \"graph-header\";\n\tconst label = document.createElement(\"span\");\n\tlabel.className = \"graph-label\";\n\tlabel.textContent = title;\n\tconst value = document.createElement(\"span\");\n\tvalue.className = \"graph-value\";\n\tvalue.style.color = color;\n\theader.append(label, value);\n\n\tconst canvas = document.createElement(\"canvas\");\n\tcanvas.className = \"graph-canvas\";\n\n\tel.append(header, canvas);\n\n\tconst samples: number[] = [];\n\t// Smoothed autoscale ceiling so the baseline doesn't jump every frame.\n\tlet scale = opts?.max ?? 1;\n\n\tconst draw = () => {\n\t\tconst ctx = canvas.getContext(\"2d\");\n\t\tif (ctx) {\n\t\t\tconst dpr = window.devicePixelRatio || 1;\n\t\t\tconst rect = canvas.getBoundingClientRect();\n\t\t\tconst w = rect.width;\n\t\t\tconst h = rect.height;\n\t\t\tconst cw = Math.round(w * dpr);\n\t\t\tconst ch = Math.round(h * dpr);\n\t\t\tif (canvas.width !== cw || canvas.height !== ch) {\n\t\t\t\tcanvas.width = cw;\n\t\t\t\tcanvas.height = ch;\n\t\t\t}\n\n\t\t\tctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\t\t\tctx.clearRect(0, 0, w, h);\n\n\t\t\tif (w > 0 && h > 0) {\n\t\t\t\t// Pick a target ceiling: fixed if given, else the rolling peak with headroom.\n\t\t\t\tconst peak = samples.length ? Math.max(...samples) : 0;\n\t\t\t\tconst target = opts?.max ?? Math.max(1, peak * 1.2);\n\t\t\t\t// Ease toward the target to avoid jitter.\n\t\t\t\tscale += (target - scale) * 0.1;\n\n\t\t\t\tconst pad = 1;\n\t\t\t\tconst usable = h - pad * 2;\n\t\t\t\tconst x = (i: number) => (capacity <= 1 ? w : (i / (capacity - 1)) * w);\n\t\t\t\tconst y = (v: number) => pad + usable - (Math.min(v, scale) / scale) * usable;\n\n\t\t\t\t// Offset so the newest sample sits at the right edge.\n\t\t\t\tconst offset = capacity - samples.length;\n\n\t\t\t\tif (samples.length > 1) {\n\t\t\t\t\tctx.beginPath();\n\t\t\t\t\tctx.moveTo(x(offset), y(samples[0]));\n\t\t\t\t\tfor (let i = 1; i < samples.length; i++) ctx.lineTo(x(offset + i), y(samples[i]));\n\n\t\t\t\t\t// Fill under the curve.\n\t\t\t\t\tconst grad = ctx.createLinearGradient(0, 0, 0, h);\n\t\t\t\t\tgrad.addColorStop(0, fillTop);\n\t\t\t\t\tgrad.addColorStop(1, fillBottom);\n\t\t\t\t\tctx.save();\n\t\t\t\t\tctx.lineTo(x(offset + samples.length - 1), h);\n\t\t\t\t\tctx.lineTo(x(offset), h);\n\t\t\t\t\tctx.closePath();\n\t\t\t\t\tctx.fillStyle = grad;\n\t\t\t\t\tctx.fill();\n\t\t\t\t\tctx.restore();\n\n\t\t\t\t\t// Stroke the line.\n\t\t\t\t\tctx.beginPath();\n\t\t\t\t\tctx.moveTo(x(offset), y(samples[0]));\n\t\t\t\t\tfor (let i = 1; i < samples.length; i++) ctx.lineTo(x(offset + i), y(samples[i]));\n\t\t\t\t\tctx.strokeStyle = color;\n\t\t\t\t\tctx.lineWidth = 1.5;\n\t\t\t\t\tctx.lineJoin = \"round\";\n\t\t\t\t\tctx.stroke();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\t// Redraw when a new sample lands (the data rate is the frame rate for a\n\t// sparkline) and when the canvas is resized. No idle animation loop.\n\tconst push = (v: number | undefined) => {\n\t\tsamples.push(v !== undefined && Number.isFinite(v) ? Math.max(0, v) : 0);\n\t\twhile (samples.length > capacity) samples.shift();\n\t\tvalue.textContent = v !== undefined && Number.isFinite(v) ? (opts?.format?.(v) ?? v.toFixed(0)) : \"—\";\n\t\tdraw();\n\t};\n\n\tif (typeof ResizeObserver !== \"undefined\") {\n\t\tconst observer = new ResizeObserver(() => draw());\n\t\tobserver.observe(canvas);\n\t\tparent.cleanup(() => observer.disconnect());\n\t}\n\n\treturn { el, push };\n}\n","import type { Effect, Getter } from \"@moq/signals\";\nimport * as DOM from \"@moq/signals/dom\";\nimport type MoqWatch from \"../element\";\nimport { formatBitrate, formatFps, formatHz, formatMillis } from \"./format\";\nimport { graph } from \"./graph\";\nimport { audio as audioIcon, icon, network as networkIcon, video as videoIcon } from \"./icons\";\n\nconst POLL_MS = 250;\n\ntype Kind = \"network\" | \"video\" | \"audio\";\n\nfunction card(kind: Kind, label: string, svg: string): { el: HTMLElement; grid: HTMLElement; status: HTMLElement } {\n\tconst el = DOM.create(\"div\", { className: `stat-card stat-card--${kind}` });\n\n\tconst head = DOM.create(\"div\", { className: \"stat-head\" });\n\tconst iconWrap = DOM.create(\"div\", { className: \"stat-icon\" });\n\ticonWrap.appendChild(icon(svg));\n\tconst status = DOM.create(\"span\", { className: \"stat-status\", style: { display: \"none\" } });\n\thead.append(iconWrap, DOM.create(\"span\", { className: \"stat-title\" }, label), status);\n\n\tconst grid = DOM.create(\"div\", { className: \"stat-grid\" });\n\tel.append(head, grid);\n\treturn { el, grid, status };\n}\n\nfunction line(grid: HTMLElement, label: string): HTMLSpanElement {\n\tconst row = DOM.create(\"div\", { className: \"stat-line\" });\n\tconst value = DOM.create(\"span\", { className: \"stat-value\" }, \"—\");\n\trow.append(DOM.create(\"span\", { className: \"stat-key\" }, label), value);\n\tgrid.appendChild(row);\n\treturn value;\n}\n\n/** Bitrate from a byte counter, sampled across an interval. */\nfunction rate(prev: { bytes: number; when: number }, bytes: number, now: number): number | undefined {\n\tif (prev.bytes <= 0) return undefined;\n\tconst elapsed = now - prev.when;\n\tconst delta = bytes - prev.bytes;\n\tif (delta <= 0 || elapsed <= 0) return undefined;\n\t// bytes → bits (*8); elapsed is ms, so *1000/elapsed gives a per-second rate.\n\treturn delta * 8 * (1000 / elapsed);\n}\n\nfunction hasRenditions(catalog: { renditions?: Record<string, unknown> } | undefined): boolean {\n\treturn Object.keys(catalog?.renditions ?? {}).length > 0;\n}\n\ninterface TrackOptions {\n\t// Hide the card entirely when this catalog has no renditions.\n\tcatalog: Getter<{ renditions?: Record<string, unknown> } | undefined>;\n\t// Show the status pill when this is true (e.g. muted / paused).\n\tflag: Getter<boolean>;\n\tlabel: string;\n}\n\n/** Hide a card when its media isn't in the catalog; show a status pill otherwise. */\nfunction track(parent: Effect, card: { el: HTMLElement; status: HTMLElement }, opts: TrackOptions) {\n\tcard.status.textContent = opts.label;\n\tparent.run((effect) => {\n\t\tconst present = hasRenditions(effect.get(opts.catalog));\n\t\tcard.el.style.display = present ? \"\" : \"none\";\n\t\tcard.status.style.display = present && effect.get(opts.flag) ? \"\" : \"none\";\n\t});\n}\n\n/** The Stats tab: live codec/network detail plus rolling graphs. */\nexport function statsTab(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst container = DOM.create(\"div\", { className: \"tab-body stats\" });\n\n\t// Video card: static detail as rows, live bitrate/fps as graphs (no duplicate rows).\n\tconst videoCard = card(\"video\", \"Video\", videoIcon);\n\tconst vRes = line(videoCard.grid, \"Resolution\");\n\tconst vCodec = line(videoCard.grid, \"Codec\");\n\tconst vBitrateGraph = graph(parent, \"Bitrate\", { color: \"#a855f7\", format: formatBitrate });\n\tconst vFpsGraph = graph(parent, \"Frame rate\", { color: \"#facc15\", format: formatFps });\n\tvideoCard.el.append(vBitrateGraph.el, vFpsGraph.el);\n\ttrack(parent, videoCard, {\n\t\tcatalog: watch.backend.video.source.catalog,\n\t\tflag: watch.backend.paused,\n\t\tlabel: \"paused\",\n\t});\n\n\t// Audio card.\n\tconst audioCard = card(\"audio\", \"Audio\", audioIcon);\n\tconst aCodec = line(audioCard.grid, \"Codec\");\n\tconst aRate2 = line(audioCard.grid, \"Sample rate\");\n\tconst aChannels = line(audioCard.grid, \"Channels\");\n\tconst aBitrate = line(audioCard.grid, \"Bitrate\");\n\ttrack(parent, audioCard, {\n\t\tcatalog: watch.backend.audio.source.catalog,\n\t\tflag: watch.backend.audio.muted,\n\t\tlabel: \"muted\",\n\t});\n\n\t// Network card: congestion-control estimate vs. the bitrate we actually pull.\n\tconst netCard = card(\"network\", \"Network\", networkIcon);\n\tconst nMax = line(netCard.grid, \"Estimated max\");\n\tconst nActual = line(netCard.grid, \"Actual\");\n\tconst nRttGraph = graph(parent, \"Round trip\", { color: \"#00dfff\", format: (v) => formatMillis(v) });\n\tnetCard.el.append(nRttGraph.el);\n\n\tcontainer.append(videoCard.el, audioCard.el, netCard.el);\n\n\tlet vPrev = { frames: 0, bytes: 0, when: performance.now() };\n\tlet aPrev = { bytes: 0, when: performance.now() };\n\n\tparent.interval(() => {\n\t\tconst now = performance.now();\n\n\t\t// Video. Resolution comes from the active rendition (catalog.display is optional).\n\t\tconst vConf = watch.backend.video.source.config.peek();\n\t\tconst vCat = watch.backend.video.source.catalog.peek();\n\t\tconst vStats = watch.backend.video.stats.peek();\n\t\tconst w = vConf?.codedWidth ?? vCat?.display?.width;\n\t\tconst h = vConf?.codedHeight ?? vCat?.display?.height;\n\t\tvRes.textContent = w && h ? `${w}×${h}` : \"—\";\n\t\tvCodec.textContent = vConf?.codec ?? \"—\";\n\n\t\tlet fps: number | undefined;\n\t\tif (vStats && vPrev.frames > 0) {\n\t\t\tconst elapsed = now - vPrev.when;\n\t\t\tconst delta = vStats.frameCount - vPrev.frames;\n\t\t\tif (delta > 0 && elapsed > 0) fps = delta / (elapsed / 1000);\n\t\t}\n\t\tconst vBitrate = vStats ? rate(vPrev, vStats.bytesReceived, now) : undefined;\n\t\tvBitrateGraph.push(vBitrate);\n\t\tvFpsGraph.push(fps);\n\t\tif (vStats) vPrev = { frames: vStats.frameCount, bytes: vStats.bytesReceived, when: now };\n\n\t\t// Audio.\n\t\tconst aConf = watch.backend.audio.source.config.peek();\n\t\tconst aStats = watch.backend.audio.stats.peek();\n\t\taCodec.textContent = aConf?.codec ?? \"—\";\n\t\taRate2.textContent = aConf?.sampleRate ? formatHz(aConf.sampleRate) : \"—\";\n\t\taChannels.textContent = aConf?.numberOfChannels ? `${aConf.numberOfChannels}` : \"—\";\n\t\tconst aBitrate2 = aStats ? rate(aPrev, aStats.bytesReceived, now) : undefined;\n\t\taBitrate.textContent = aBitrate2 !== undefined ? formatBitrate(aBitrate2) : \"—\";\n\t\tif (aStats) aPrev = { bytes: aStats.bytesReceived, when: now };\n\n\t\t// Network. \"Estimated max\" is the congestion controller / PROBE estimate;\n\t\t// \"Actual\" is the goodput we measure from the video + audio byte counters.\n\t\tconst conn = watch.connection.established.peek();\n\t\tconst estimate = conn?.recvBandwidth?.peek();\n\t\tnMax.textContent = estimate ? formatBitrate(estimate) : \"—\";\n\t\tconst actual =\n\t\t\tvBitrate !== undefined || aBitrate2 !== undefined ? (vBitrate ?? 0) + (aBitrate2 ?? 0) : undefined;\n\t\tnActual.textContent = actual !== undefined ? formatBitrate(actual) : \"—\";\n\t\tconst rtt = conn?.rtt?.peek();\n\t\tnRttGraph.push(rtt !== undefined && rtt > 0 ? rtt : undefined);\n\t}, POLL_MS);\n\n\treturn container;\n}\n","import type { Moq } from \"@moq/hang\";\nimport type { Effect } from \"@moq/signals\";\nimport type { BufferedRanges } from \"../..\";\nimport type MoqWatch from \"../../element\";\n\nconst MIN_RANGE = 0 as Moq.Time.Milli;\nconst RANGE_STEP = 10 as Moq.Time.Milli;\nconst DEFAULT_MAX = 4000 as Moq.Time.Milli;\nconst LABEL_WIDTH = 48;\n\nfunction drawRanges(\n\tcanvas: HTMLCanvasElement,\n\tranges: BufferedRanges,\n\ttimestamp: Moq.Time.Milli | undefined,\n\tmax: Moq.Time.Milli,\n\tisBuffering: boolean,\n) {\n\tconst ctx = canvas.getContext(\"2d\");\n\tif (!ctx) return;\n\n\tconst dpr = window.devicePixelRatio || 1;\n\tconst rect = canvas.getBoundingClientRect();\n\tconst width = rect.width;\n\tconst height = rect.height;\n\n\tconst canvasW = Math.round(width * dpr);\n\tconst canvasH = Math.round(height * dpr);\n\tif (canvas.width !== canvasW || canvas.height !== canvasH) {\n\t\tcanvas.width = canvasW;\n\t\tcanvas.height = canvasH;\n\t}\n\n\tctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\tctx.clearRect(0, 0, width, height);\n\n\tif (timestamp === undefined) return;\n\n\tconst padding = 2;\n\tconst rangeHeight = height - padding * 2;\n\tconst radius = 2;\n\n\tfor (let i = 0; i < ranges.length; i++) {\n\t\tconst range = ranges[i];\n\t\tconst startMs = (range.start - timestamp) as Moq.Time.Milli;\n\t\tconst endMs = (range.end - timestamp) as Moq.Time.Milli;\n\t\tconst visibleStart = Math.max(0, startMs);\n\t\tconst visibleEnd = Math.min(endMs, max);\n\n\t\tif (visibleEnd <= visibleStart) continue;\n\n\t\tconst x = (visibleStart / max) * width;\n\t\tconst w = Math.max(2, ((visibleEnd - visibleStart) / max) * width);\n\n\t\tctx.globalAlpha = 0.85;\n\t\tctx.fillStyle = isBuffering ? \"#f87171\" : i > 0 ? \"#facc15\" : \"#4ade80\";\n\n\t\tif (typeof ctx.roundRect === \"function\") {\n\t\t\tctx.beginPath();\n\t\t\tctx.roundRect(x, padding, w, rangeHeight, radius);\n\t\t\tctx.fill();\n\t\t} else {\n\t\t\tctx.fillRect(x, padding, w, rangeHeight);\n\t\t}\n\n\t\tif (endMs > max) {\n\t\t\tconst overflowSec = ((endMs - max) / 1000).toFixed(1);\n\t\t\tctx.globalAlpha = 0.7;\n\t\t\tctx.fillStyle = \"black\";\n\t\t\tctx.font = \"500 9px system-ui, sans-serif\";\n\t\t\tctx.textAlign = \"right\";\n\t\t\tctx.textBaseline = \"middle\";\n\t\t\tctx.fillText(`+${overflowSec}s`, x + w - 4, height / 2);\n\t\t}\n\t}\n}\n\nexport function bufferControl(parent: Effect, watch: MoqWatch, max: Moq.Time.Milli = DEFAULT_MAX): HTMLElement {\n\tconst wrapper = document.createElement(\"div\");\n\twrapper.className = \"buffer\";\n\n\tconst viz = document.createElement(\"div\");\n\tviz.className = \"buffer-visualization\";\n\tviz.setAttribute(\"role\", \"slider\");\n\tviz.tabIndex = 0;\n\tviz.setAttribute(\"aria-valuemin\", MIN_RANGE.toString());\n\tviz.setAttribute(\"aria-valuemax\", max.toString());\n\tviz.setAttribute(\"aria-label\", \"Buffer jitter\");\n\n\tconst playhead = document.createElement(\"div\");\n\tplayhead.className = \"buffer-playhead\";\n\n\tconst videoTrack = document.createElement(\"div\");\n\tvideoTrack.className = \"buffer-track buffer-track--video\";\n\tconst videoLabel = document.createElement(\"span\");\n\tvideoLabel.className = \"buffer-track-label\";\n\tvideoLabel.textContent = \"Video\";\n\tconst videoCanvas = document.createElement(\"canvas\");\n\tvideoCanvas.className = \"buffer-canvas\";\n\tvideoTrack.append(videoLabel, videoCanvas);\n\n\tconst audioTrack = document.createElement(\"div\");\n\taudioTrack.className = \"buffer-track buffer-track--audio\";\n\tconst audioLabel = document.createElement(\"span\");\n\taudioLabel.className = \"buffer-track-label\";\n\taudioLabel.textContent = \"Audio\";\n\tconst audioCanvas = document.createElement(\"canvas\");\n\taudioCanvas.className = \"buffer-canvas\";\n\taudioTrack.append(audioLabel, audioCanvas);\n\n\tconst targetArea = document.createElement(\"div\");\n\ttargetArea.className = \"buffer-target-area\";\n\tconst targetLine = document.createElement(\"div\");\n\ttargetLine.className = \"buffer-target-line\";\n\tconst targetLabel = document.createElement(\"span\");\n\ttargetLabel.className = \"buffer-target-label\";\n\ttargetLine.appendChild(targetLabel);\n\ttargetArea.appendChild(targetLine);\n\n\tconst help = document.createElement(\"span\");\n\thelp.className = \"buffer-help\";\n\thelp.textContent = \"click to change latency\";\n\n\tviz.append(playhead, videoTrack, audioTrack, targetArea, help);\n\twrapper.appendChild(viz);\n\n\tlet dragging = false;\n\tlet hasInteracted = false;\n\n\tparent.run((effect) => {\n\t\tconst jitter = effect.get(watch.backend.jitter);\n\t\tconst pct = (jitter / max) * 100;\n\t\ttargetLine.style.left = `${pct}%`;\n\t\ttargetLabel.textContent = `${Math.round(jitter)}ms`;\n\t\tviz.setAttribute(\"aria-valuenow\", jitter.toString());\n\t});\n\n\tconst updateFromX = (clientX: number) => {\n\t\tconst rect = viz.getBoundingClientRect();\n\t\tconst trackWidth = rect.width - LABEL_WIDTH;\n\t\tconst x = Math.max(0, Math.min(clientX - rect.left - LABEL_WIDTH, trackWidth));\n\t\tconst ms = (x / trackWidth) * max;\n\t\tconst snapped = (Math.round(ms / RANGE_STEP) * RANGE_STEP) as Moq.Time.Milli;\n\t\tconst clamped = Math.max(MIN_RANGE, Math.min(max, snapped)) as Moq.Time.Milli;\n\t\twatch.backend.latency.set(clamped);\n\t};\n\n\tconst interact = () => {\n\t\tif (!hasInteracted) {\n\t\t\thasInteracted = true;\n\t\t\thelp.style.display = \"none\";\n\t\t}\n\t};\n\n\tparent.event(viz, \"mousedown\", (e) => {\n\t\tdragging = true;\n\t\tviz.classList.add(\"buffer-visualization--dragging\");\n\t\tinteract();\n\t\tupdateFromX(e.clientX);\n\t});\n\n\tparent.event(document, \"mousemove\", (e) => {\n\t\tif (dragging) updateFromX(e.clientX);\n\t});\n\n\tparent.event(document, \"mouseup\", () => {\n\t\tif (!dragging) return;\n\t\tdragging = false;\n\t\tviz.classList.remove(\"buffer-visualization--dragging\");\n\t});\n\n\tparent.event(viz, \"keydown\", (e) => {\n\t\tlet delta = 0 as Moq.Time.Milli;\n\t\tif (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") {\n\t\t\tdelta = RANGE_STEP;\n\t\t} else if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") {\n\t\t\tdelta = -RANGE_STEP as Moq.Time.Milli;\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\t\te.preventDefault();\n\t\tinteract();\n\t\tconst current = watch.backend.jitter.peek();\n\t\tconst value = Math.max(MIN_RANGE, Math.min(max, current + delta)) as Moq.Time.Milli;\n\t\twatch.backend.latency.set(value);\n\t});\n\n\tconst draw = () => {\n\t\tconst timestamp = watch.backend.sync.now();\n\t\tconst isBuffering = watch.backend.video.stalled.peek();\n\t\tdrawRanges(videoCanvas, watch.backend.video.buffered.peek(), timestamp, max, isBuffering);\n\t\tdrawRanges(audioCanvas, watch.backend.audio.buffered.peek(), timestamp, max, isBuffering);\n\t\tparent.animate(draw);\n\t};\n\tparent.animate(draw);\n\n\treturn wrapper;\n}\n","import type { Moq } from \"@moq/hang\";\nimport type { Effect } from \"@moq/signals\";\nimport * as DOM from \"@moq/signals/dom\";\nimport type MoqWatch from \"../../element\";\nimport { formatMillis } from \"../format\";\nimport { bufferControl } from \"./buffer-control\";\n\ntype Preset = { label: string; value: \"real-time\" | number };\n\nconst PRESETS: Preset[] = [\n\t{ label: \"Real-time\", value: \"real-time\" },\n\t{ label: \"100ms\", value: 100 },\n\t{ label: \"250ms\", value: 250 },\n\t{ label: \"500ms\", value: 500 },\n\t{ label: \"1s\", value: 1000 },\n\t{ label: \"2s\", value: 2000 },\n];\n\n/** The Latency tab: choose a buffer target and watch the live buffer timeline. */\nexport function latencyTab(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst container = DOM.create(\"div\", { className: \"tab-body latency\" });\n\n\t// Preset chips.\n\tconst chips = DOM.create(\"div\", { className: \"latency-presets\" });\n\tconst buttons = PRESETS.map((preset) => {\n\t\tconst chip = DOM.create(\"button\", { className: \"chip\", type: \"button\" }, preset.label);\n\t\tparent.event(chip, \"click\", () => {\n\t\t\twatch.backend.latency.set(preset.value === \"real-time\" ? \"real-time\" : (preset.value as Moq.Time.Milli));\n\t\t});\n\t\tchips.appendChild(chip);\n\t\treturn { preset, chip };\n\t});\n\n\tparent.run((effect) => {\n\t\tconst mode = effect.get(watch.backend.latency);\n\t\tfor (const { preset, chip } of buttons) {\n\t\t\tconst active = preset.value === \"real-time\" ? mode === \"real-time\" : mode === preset.value;\n\t\t\tchip.classList.toggle(\"chip--active\", active);\n\t\t}\n\t});\n\n\t// The draggable buffered-range timeline.\n\tconst timeline = bufferControl(parent, watch);\n\n\t// Numeric readout: resolved jitter + total end-to-end buffer.\n\tconst readout = DOM.create(\"div\", { className: \"latency-readout\" });\n\tconst jitterStat = DOM.create(\"div\", { className: \"latency-stat\" });\n\tconst jitterVal = DOM.create(\"span\", { className: \"latency-stat-value\" }, \"—\");\n\tjitterStat.append(DOM.create(\"span\", { className: \"latency-stat-label\" }, \"Jitter buffer\"), jitterVal);\n\tconst bufferStat = DOM.create(\"div\", { className: \"latency-stat\" });\n\tconst bufferVal = DOM.create(\"span\", { className: \"latency-stat-value\" }, \"—\");\n\tbufferStat.append(DOM.create(\"span\", { className: \"latency-stat-label\" }, \"Total buffer\"), bufferVal);\n\treadout.append(jitterStat, bufferStat);\n\n\tparent.run((effect) => {\n\t\tconst mode = effect.get(watch.backend.latency);\n\t\tconst jitter = effect.get(watch.backend.jitter);\n\t\tconst total = effect.get(watch.backend.sync.buffer);\n\t\tjitterVal.textContent = `${formatMillis(jitter)}${mode === \"real-time\" ? \" (auto)\" : \"\"}`;\n\t\tbufferVal.textContent = formatMillis(total);\n\t});\n\n\tconst hint = DOM.create(\n\t\t\"div\",\n\t\t{ className: \"tab-hint\" },\n\t\t\"A larger buffer smooths over network jitter at the cost of latency. Real-time tracks the connection RTT automatically. Drag the timeline to fine-tune.\",\n\t);\n\n\tcontainer.append(chips, timeline, readout, hint);\n\treturn container;\n}\n","import type { Catalog } from \"@moq/hang\";\nimport type { Effect } from \"@moq/signals\";\nimport * as DOM from \"@moq/signals/dom\";\nimport type MoqWatch from \"../../element\";\nimport { formatBitrate } from \"../format\";\nimport { check, icon } from \"../icons\";\n\nfunction resolution(config: Catalog.VideoConfig): string | undefined {\n\tif (config.codedWidth && config.codedHeight) return `${config.codedWidth}×${config.codedHeight}`;\n\treturn undefined;\n}\n\nfunction detail(config: Catalog.VideoConfig): string {\n\tconst parts: string[] = [];\n\tif (config.framerate) parts.push(`${Math.round(config.framerate)}fps`);\n\tif (config.bitrate) parts.push(formatBitrate(config.bitrate));\n\tif (config.codec) parts.push(config.codec);\n\treturn parts.join(\" · \");\n}\n\nfunction qualityRow(\n\tparent: Effect,\n\ttitle: string,\n\tsubtitle: string,\n\tselected: boolean,\n\tplaying: boolean,\n\tonSelect: () => void,\n): HTMLElement {\n\tconst row = DOM.create(\"button\", { className: \"q-row\", type: \"button\" });\n\tif (selected) row.classList.add(\"q-row--selected\");\n\n\tconst mark = DOM.create(\"span\", { className: \"q-row-check\" });\n\tif (selected) mark.appendChild(icon(check));\n\n\tconst body = DOM.create(\"div\", { className: \"q-row-body\" });\n\tconst titleEl = DOM.create(\"span\", { className: \"q-row-title\" }, title);\n\tif (playing) {\n\t\tconst badge = DOM.create(\"span\", { className: \"q-row-playing\" }, \"playing\");\n\t\ttitleEl.appendChild(badge);\n\t}\n\tconst subEl = DOM.create(\"span\", { className: \"q-row-sub\" }, subtitle);\n\tbody.append(titleEl, subEl);\n\n\trow.append(mark, body);\n\tparent.event(row, \"click\", onSelect);\n\treturn row;\n}\n\n/** The Quality tab: pick a video rendition or let ABR choose automatically. */\nexport function qualityTab(parent: Effect, watch: MoqWatch): HTMLElement {\n\tconst container = DOM.create(\"div\", { className: \"tab-body q-list\" });\n\n\tparent.run((effect) => {\n\t\tconst source = watch.backend.video.source;\n\t\tconst catalog = effect.get(source.catalog);\n\t\tconst target = effect.get(source.target);\n\t\tconst active = effect.get(source.track);\n\t\tconst renditions = catalog?.renditions ?? {};\n\t\tconst entries = Object.entries(renditions);\n\n\t\tcontainer.replaceChildren();\n\n\t\tif (entries.length === 0) {\n\t\t\tcontainer.appendChild(DOM.create(\"div\", { className: \"tab-empty\" }, \"No video renditions\"));\n\t\t\treturn;\n\t\t}\n\n\t\tconst manual = target?.name;\n\n\t\t// Auto row: ABR picks the best rendition for the available bandwidth.\n\t\tconst autoSub = active && !manual ? `auto · currently ${active}` : \"adapts to bandwidth\";\n\t\tcontainer.appendChild(\n\t\t\tqualityRow(effect, \"Auto\", autoSub, !manual, false, () => {\n\t\t\t\tsource.target.update((prev) => ({ ...prev, name: undefined }));\n\t\t\t}),\n\t\t);\n\n\t\t// Largest first so the list reads top-down high to low.\n\t\tentries.sort((a, b) => {\n\t\t\tconst sa = (a[1].codedWidth ?? 0) * (a[1].codedHeight ?? 0);\n\t\t\tconst sb = (b[1].codedWidth ?? 0) * (b[1].codedHeight ?? 0);\n\t\t\treturn sb - sa;\n\t\t});\n\n\t\tfor (const [name, config] of entries) {\n\t\t\tconst title = resolution(config) ?? name;\n\t\t\tcontainer.appendChild(\n\t\t\t\tqualityRow(effect, title, detail(config), manual === name, active === name, () => {\n\t\t\t\t\tsource.target.update((prev) => ({ ...prev, name }));\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t});\n\n\treturn container;\n}\n","import type { Effect } from \"@moq/signals\";\nimport * as DOM from \"@moq/signals/dom\";\nimport type MoqWatch from \"../../element\";\nimport { close, gauge, icon, settings as settingsIcon, stats as statsIcon, video as videoIcon } from \"../icons\";\nimport type { Tab, UiState } from \"../state\";\nimport { statsTab } from \"../stats\";\nimport { latencyTab } from \"./latency\";\nimport { qualityTab } from \"./quality\";\n\nconst TABS: { id: Tab; label: string; svg: string }[] = [\n\t{ id: \"quality\", label: \"Quality\", svg: videoIcon },\n\t{ id: \"latency\", label: \"Latency\", svg: gauge },\n\t{ id: \"stats\", label: \"Stats\", svg: statsIcon },\n];\n\n/** The slide-in settings sheet: tab strip + the active tab's content. */\nexport function settingsPanel(parent: Effect, watch: MoqWatch, state: UiState): HTMLElement {\n\tconst panel = DOM.create(\"div\", { className: \"panel\" });\n\tpanel.setAttribute(\"role\", \"dialog\");\n\tpanel.setAttribute(\"aria-label\", \"Player settings\");\n\n\t// Header.\n\tconst header = DOM.create(\"div\", { className: \"panel-head\" });\n\tconst titleWrap = DOM.create(\"div\", { className: \"panel-head-title flex-align-center\" });\n\ttitleWrap.append(icon(settingsIcon), DOM.create(\"span\", {}, \"Settings\"));\n\tconst closeBtn = DOM.create(\"button\", { className: \"control flex-center\", type: \"button\" });\n\tcloseBtn.title = \"Close\";\n\tcloseBtn.setAttribute(\"aria-label\", \"Close settings\");\n\tcloseBtn.appendChild(icon(close));\n\tparent.event(closeBtn, \"click\", () => state.panel.set(false));\n\theader.append(titleWrap, closeBtn);\n\n\t// Tab strip (ARIA tablist). IDs are scoped to this shadow root.\n\tconst PANEL_ID = \"panel-body\";\n\tconst strip = DOM.create(\"div\", { className: \"panel-tabs\" });\n\tstrip.setAttribute(\"role\", \"tablist\");\n\tconst tabButtons = TABS.map((tab) => {\n\t\tconst btn = DOM.create(\"button\", { className: \"panel-tab\", type: \"button\", id: `tab-${tab.id}` });\n\t\tbtn.setAttribute(\"role\", \"tab\");\n\t\tbtn.setAttribute(\"aria-controls\", PANEL_ID);\n\t\tbtn.append(icon(tab.svg), DOM.create(\"span\", {}, tab.label));\n\t\tparent.event(btn, \"click\", () => state.tab.set(tab.id));\n\t\tstrip.appendChild(btn);\n\t\treturn { tab, btn };\n\t});\n\n\t// Body: mount only the active tab while the panel is open so hidden tabs\n\t// stop polling and animating.\n\tconst body = DOM.create(\"div\", { className: \"panel-body\", id: PANEL_ID });\n\tbody.setAttribute(\"role\", \"tabpanel\");\n\n\tparent.run((effect) => {\n\t\tconst active = effect.get(state.tab);\n\t\tbody.setAttribute(\"aria-labelledby\", `tab-${active}`);\n\t\tfor (const { tab, btn } of tabButtons) {\n\t\t\tconst selected = tab.id === active;\n\t\t\tbtn.classList.toggle(\"panel-tab--active\", selected);\n\t\t\tbtn.setAttribute(\"aria-selected\", String(selected));\n\t\t\tbtn.tabIndex = selected ? 0 : -1;\n\t\t}\n\t});\n\tparent.run((effect) => {\n\t\tconst open = effect.get(state.panel);\n\t\tif (!open) return;\n\t\tconst tab = effect.get(state.tab);\n\t\tconst content =\n\t\t\ttab === \"quality\"\n\t\t\t\t? qualityTab(effect, watch)\n\t\t\t\t: tab === \"latency\"\n\t\t\t\t\t? latencyTab(effect, watch)\n\t\t\t\t\t: statsTab(effect, watch);\n\t\tDOM.render(effect, body, content);\n\t});\n\n\tpanel.append(header, strip, body);\n\n\tparent.run((effect) => {\n\t\tconst open = effect.get(state.panel);\n\t\tpanel.classList.toggle(\"panel--open\", open);\n\t\tpanel.setAttribute(\"aria-hidden\", String(!open));\n\t});\n\n\treturn panel;\n}\n","@import \"./base.css\";\n@import \"./player.css\";\n@import \"./controls.css\";\n@import \"./panel.css\";\n@import \"./buffer-control.css\";\n@import \"./buffering-indicator.css\";\n@import \"./offline-indicator.css\";\n","import { Effect, Signal } from \"@moq/signals\";\nimport * as DOM from \"@moq/signals/dom\";\nimport type MoqWatch from \"../element\";\nimport { bufferingIndicator } from \"./components/buffering-indicator\";\nimport { centerPlay } from \"./components/center-play\";\nimport { controlBar } from \"./components/control-bar\";\nimport { offlineIndicator } from \"./components/offline-indicator\";\nimport { settingsPanel } from \"./components/settings-panel\";\nimport type { Tab, UiState } from \"./state\";\nimport styles from \"./styles/index.css?inline\";\n\n// How long the chrome lingers after the pointer stops moving (while playing).\nconst HIDE_MS = 2800;\n\nexport default class MoqWatchUi extends HTMLElement {\n\t#signals?: Effect;\n\t#root: ShadowRoot;\n\t#watch = new Signal<MoqWatch | undefined>(undefined);\n\t#observer: MutationObserver;\n\n\tconstructor() {\n\t\tsuper();\n\t\tthis.#root = this.attachShadow({ mode: \"open\" });\n\n\t\tconst style = document.createElement(\"style\");\n\t\tstyle.textContent = styles;\n\t\tthis.#root.appendChild(style);\n\n\t\tthis.#observer = new MutationObserver(() => this.#updateWatch());\n\t}\n\n\tconnectedCallback() {\n\t\tthis.#updateWatch();\n\t\tthis.#observer.observe(this, { childList: true });\n\n\t\tconst signals = new Effect();\n\t\tthis.#signals = signals;\n\t\tsignals.run(this.#render.bind(this));\n\t}\n\n\tdisconnectedCallback() {\n\t\tthis.#observer.disconnect();\n\t\tthis.#signals?.close();\n\t\tthis.#signals = undefined;\n\t}\n\n\t#updateWatch() {\n\t\tconst watch = this.querySelector(\"moq-watch\") as MoqWatch | null;\n\t\tthis.#watch.set(watch ?? undefined);\n\t}\n\n\t#render(effect: Effect) {\n\t\tconst watch = effect.get(this.#watch);\n\t\tif (!watch) return;\n\n\t\tconst state: UiState = {\n\t\t\tchrome: new Signal(true),\n\t\t\tpanel: new Signal(false),\n\t\t\ttab: new Signal<Tab>(\"quality\"),\n\t\t};\n\n\t\tconst player = DOM.create(\"div\", { className: \"player\" });\n\n\t\t// The slotted <moq-watch> (canvas/video) sits at the base of the stack.\n\t\tplayer.appendChild(DOM.create(\"slot\"));\n\n\t\t// Center affordances: play prompt + buffering spinner + offline notice.\n\t\tconst center = DOM.create(\"div\", { className: \"center\" });\n\t\tcenter.append(centerPlay(effect, watch), bufferingIndicator(effect, watch), offlineIndicator(effect, watch));\n\n\t\t// Top scrim keeps the bottom bar legible and hosts ambient gradient.\n\t\tconst scrimTop = DOM.create(\"div\", { className: \"scrim scrim--top\" });\n\n\t\t// Bottom chrome: gradient scrim + the control bar.\n\t\tconst chrome = DOM.create(\"div\", { className: \"chrome\" });\n\t\tchrome.append(\n\t\t\tDOM.create(\"div\", { className: \"scrim scrim--bottom\" }),\n\t\t\tcontrolBar(effect, watch, state, player),\n\t\t);\n\n\t\tconst panel = settingsPanel(effect, watch, state);\n\n\t\tplayer.append(scrimTop, center, chrome, panel);\n\t\tDOM.render(effect, this.#root, player);\n\n\t\tthis.#wireChrome(effect, watch, state, player);\n\t}\n\n\t// Show the chrome on activity, auto-hide while playing once the pointer\n\t// settles. Stays pinned while paused or when the settings panel is open.\n\t#wireChrome(effect: Effect, watch: MoqWatch, state: UiState, player: HTMLElement) {\n\t\t// Bump on any pointer/focus activity to re-arm the auto-hide.\n\t\tconst activity = new Signal(0);\n\t\tconst bump = () => activity.update((n) => n + 1);\n\t\teffect.event(this, \"pointermove\", bump);\n\t\teffect.event(this, \"pointerdown\", bump);\n\t\teffect.event(this, \"focusin\", bump);\n\n\t\tconst pinned = () => watch.backend.paused.peek() || state.panel.peek();\n\n\t\t// Reveal on activity and reschedule the hide timer. Reruns when pinned\n\t\t// state changes too, so leaving pinned (e.g. closing settings while\n\t\t// playing) re-arms the auto-hide. effect.timer auto-clears on rerun.\n\t\teffect.run((e) => {\n\t\t\tconst isPinned = e.get(watch.backend.paused) || e.get(state.panel);\n\t\t\te.get(activity);\n\t\t\tstate.chrome.set(true);\n\t\t\tif (isPinned) return;\n\t\t\te.timer(() => state.chrome.set(false), HIDE_MS);\n\t\t});\n\n\t\teffect.event(this, \"pointerleave\", () => {\n\t\t\tif (pinned()) return;\n\t\t\tstate.chrome.set(false);\n\t\t});\n\n\t\teffect.run((e) => {\n\t\t\tplayer.classList.toggle(\"player--chrome\", e.get(state.chrome));\n\t\t});\n\t}\n}\n\ncustomElements.define(\"moq-watch-ui\", MoqWatchUi);\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t\"moq-watch-ui\": MoqWatchUi;\n\t}\n}\n"],"mappings":"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"}
|
package/ui/format.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Format a bitrate (bits per second) as Mbps/kbps/bps. */
|
|
2
|
+
export declare function formatBitrate(bps: number): string;
|
|
3
|
+
/** Format a bandwidth estimate with a direction arrow, or null if unavailable. */
|
|
4
|
+
export declare function formatBandwidth(bps: number | undefined, dir: "up" | "down"): string | null;
|
|
5
|
+
/** Format a duration in milliseconds, switching to seconds past 1s. */
|
|
6
|
+
export declare function formatMillis(ms: number): string;
|
|
7
|
+
/** Format an audio sample rate (Hz) as kHz. */
|
|
8
|
+
export declare function formatHz(hz: number): string;
|
|
9
|
+
/** Format a frame rate. */
|
|
10
|
+
export declare function formatFps(fps: number): string;
|
|
11
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/ui/format.ts"],"names":[],"mappings":"AAEA,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIjD;AAED,kFAAkF;AAClF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAK1F;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAG/C;AAED,+CAA+C;AAC/C,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED,2BAA2B;AAC3B,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Effect } from "@moq/signals";
|
|
2
|
+
export interface Fullscreen {
|
|
3
|
+
/** Whether the player is currently maximized (real or pseudo fullscreen). */
|
|
4
|
+
active(): boolean;
|
|
5
|
+
/** Enter if windowed, exit if fullscreen. Must run inside a user gesture. */
|
|
6
|
+
toggle(): void;
|
|
7
|
+
/** Subscribe to state changes; returns an unsubscribe function. */
|
|
8
|
+
onChange(fn: () => void): () => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* @param parent Effect that owns the document listeners (auto-removed on cleanup).
|
|
12
|
+
* @param player The shadow container to maximize.
|
|
13
|
+
* @param media Resolves the current <canvas>/<video>, used for the iOS path.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createFullscreen(parent: Effect, player: HTMLElement, media: () => HTMLElement | undefined): Fullscreen;
|
|
16
|
+
//# sourceMappingURL=fullscreen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fullscreen.d.ts","sourceRoot":"","sources":["../../src/ui/fullscreen.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAqB3C,MAAM,WAAW,UAAU;IAC1B,6EAA6E;IAC7E,MAAM,IAAI,OAAO,CAAC;IAClB,6EAA6E;IAC7E,MAAM,IAAI,IAAI,CAAC;IACf,mEAAmE;IACnE,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CACrC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,WAAW,GAAG,SAAS,GAClC,UAAU,CA6EZ"}
|
package/ui/graph.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Effect } from "@moq/signals";
|
|
2
|
+
export interface GraphOptions {
|
|
3
|
+
max?: number;
|
|
4
|
+
samples?: number;
|
|
5
|
+
color?: string;
|
|
6
|
+
format?: (v: number) => string;
|
|
7
|
+
}
|
|
8
|
+
export interface Graph {
|
|
9
|
+
el: HTMLElement;
|
|
10
|
+
push(value: number | undefined): void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A rolling time-series sparkline. Samples scroll right-to-left and the area
|
|
14
|
+
* under the line is filled with a fading gradient. Redraws are event-driven:
|
|
15
|
+
* each `push` and any canvas resize triggers a repaint (no animation loop).
|
|
16
|
+
*/
|
|
17
|
+
export declare function graph(parent: Effect, title: string, opts?: GraphOptions): Graph;
|
|
18
|
+
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/ui/graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,WAAW,YAAY;IAE5B,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,KAAK;IACrB,EAAE,EAAE,WAAW,CAAC;IAEhB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;CACtC;AAmBD;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,KAAK,CA8G/E"}
|
package/ui/icons.d.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import audio from "./icons/audio.svg?raw";
|
|
2
2
|
import buffer from "./icons/buffer.svg?raw";
|
|
3
|
+
import check from "./icons/check.svg?raw";
|
|
4
|
+
import close from "./icons/close.svg?raw";
|
|
3
5
|
import fullscreenEnter from "./icons/fullscreen-enter.svg?raw";
|
|
4
6
|
import fullscreenExit from "./icons/fullscreen-exit.svg?raw";
|
|
7
|
+
import gauge from "./icons/gauge.svg?raw";
|
|
5
8
|
import mute from "./icons/mute.svg?raw";
|
|
6
9
|
import network from "./icons/network.svg?raw";
|
|
7
10
|
import pause from "./icons/pause.svg?raw";
|
|
8
11
|
import play from "./icons/play.svg?raw";
|
|
12
|
+
import settings from "./icons/settings.svg?raw";
|
|
9
13
|
import stats from "./icons/stats.svg?raw";
|
|
10
14
|
import video from "./icons/video.svg?raw";
|
|
11
15
|
import volumeHigh from "./icons/volume-high.svg?raw";
|
|
12
16
|
import volumeLow from "./icons/volume-low.svg?raw";
|
|
13
17
|
import volumeMedium from "./icons/volume-medium.svg?raw";
|
|
14
|
-
export { audio, buffer, fullscreenEnter, fullscreenExit, mute, network, pause, play, stats, video, volumeHigh, volumeLow, volumeMedium, };
|
|
18
|
+
export { audio, buffer, check, close, fullscreenEnter, fullscreenExit, gauge, mute, network, pause, play, settings, stats, video, volumeHigh, volumeLow, volumeMedium, };
|
|
15
19
|
export declare function icon(svg: string): HTMLElement;
|
|
16
20
|
//# sourceMappingURL=icons.d.ts.map
|
package/ui/icons.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../../src/ui/icons.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,uBAAuB,CAAC;AAC1C,OAAO,MAAM,MAAM,wBAAwB,CAAC;AAC5C,OAAO,eAAe,MAAM,kCAAkC,CAAC;AAC/D,OAAO,cAAc,MAAM,iCAAiC,CAAC;AAC7D,OAAO,IAAI,MAAM,sBAAsB,CAAC;AACxC,OAAO,OAAO,MAAM,yBAAyB,CAAC;AAC9C,OAAO,KAAK,MAAM,uBAAuB,CAAC;AAC1C,OAAO,IAAI,MAAM,sBAAsB,CAAC;AACxC,OAAO,KAAK,MAAM,uBAAuB,CAAC;AAC1C,OAAO,KAAK,MAAM,uBAAuB,CAAC;AAC1C,OAAO,UAAU,MAAM,6BAA6B,CAAC;AACrD,OAAO,SAAS,MAAM,4BAA4B,CAAC;AACnD,OAAO,YAAY,MAAM,+BAA+B,CAAC;AAEzD,OAAO,EACN,KAAK,EACL,MAAM,EACN,eAAe,EACf,cAAc,EACd,IAAI,EACJ,OAAO,EACP,KAAK,EACL,IAAI,EACJ,KAAK,EACL,KAAK,EACL,UAAU,EACV,SAAS,EACT,YAAY,GACZ,CAAC;AAEF,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAO7C"}
|
|
1
|
+
{"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../../src/ui/icons.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,uBAAuB,CAAC;AAC1C,OAAO,MAAM,MAAM,wBAAwB,CAAC;AAC5C,OAAO,KAAK,MAAM,uBAAuB,CAAC;AAC1C,OAAO,KAAK,MAAM,uBAAuB,CAAC;AAC1C,OAAO,eAAe,MAAM,kCAAkC,CAAC;AAC/D,OAAO,cAAc,MAAM,iCAAiC,CAAC;AAC7D,OAAO,KAAK,MAAM,uBAAuB,CAAC;AAC1C,OAAO,IAAI,MAAM,sBAAsB,CAAC;AACxC,OAAO,OAAO,MAAM,yBAAyB,CAAC;AAC9C,OAAO,KAAK,MAAM,uBAAuB,CAAC;AAC1C,OAAO,IAAI,MAAM,sBAAsB,CAAC;AACxC,OAAO,QAAQ,MAAM,0BAA0B,CAAC;AAChD,OAAO,KAAK,MAAM,uBAAuB,CAAC;AAC1C,OAAO,KAAK,MAAM,uBAAuB,CAAC;AAC1C,OAAO,UAAU,MAAM,6BAA6B,CAAC;AACrD,OAAO,SAAS,MAAM,4BAA4B,CAAC;AACnD,OAAO,YAAY,MAAM,+BAA+B,CAAC;AAEzD,OAAO,EACN,KAAK,EACL,MAAM,EACN,KAAK,EACL,KAAK,EACL,eAAe,EACf,cAAc,EACd,KAAK,EACL,IAAI,EACJ,OAAO,EACP,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,KAAK,EACL,KAAK,EACL,UAAU,EACV,SAAS,EACT,YAAY,GACZ,CAAC;AAEF,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAO7C"}
|
package/ui/state.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Signal } from "@moq/signals";
|
|
2
|
+
export type Tab = "quality" | "latency" | "stats";
|
|
3
|
+
/** Shared reactive UI chrome state, threaded through the control components. */
|
|
4
|
+
export interface UiState {
|
|
5
|
+
chrome: Signal<boolean>;
|
|
6
|
+
panel: Signal<boolean>;
|
|
7
|
+
tab: Signal<Tab>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/ui/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,MAAM,GAAG,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAElD,gFAAgF;AAChF,MAAM,WAAW,OAAO;IAEvB,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAExB,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAEvB,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;CACjB"}
|
package/ui/stats.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Effect
|
|
1
|
+
import type { Effect } from "@moq/signals";
|
|
2
2
|
import type MoqWatch from "../element";
|
|
3
|
-
|
|
3
|
+
/** The Stats tab: live codec/network detail plus rolling graphs. */
|
|
4
|
+
export declare function statsTab(parent: Effect, watch: MoqWatch): HTMLElement;
|
|
4
5
|
//# sourceMappingURL=stats.d.ts.map
|
package/ui/stats.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../src/ui/stats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../src/ui/stats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAU,MAAM,cAAc,CAAC;AAEnD,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAC;AA+DvC,oEAAoE;AACpE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,WAAW,CAsFrE"}
|