diff --git a/ui/src/components/ConfigEditor.jsx b/ui/src/components/ConfigEditor.jsx index aae8c2f..c02d2e8 100644 --- a/ui/src/components/ConfigEditor.jsx +++ b/ui/src/components/ConfigEditor.jsx @@ -55,32 +55,67 @@ function InputDeviceField({ config, ndiSources, ndiSourcesBusy, onRefreshNdiSour return ; } + const currentDevice = readPath(config, "input.device") ?? ""; + const presetOptions = ["default", "auto"]; + const discoveredOptions = Array.from(new Set(ndiSources.map((source) => source.name).filter(Boolean))).filter( + (name) => !presetOptions.includes(name) + ); + const selectOptions = [...presetOptions, ...discoveredOptions]; + const selectValue = selectOptions.includes(currentDevice) ? currentDevice : currentDevice ? "__current__" : "default"; + return ( -
+
+
+ + +
setConfig((current) => writePath(current, "input.device", event.target.value))} /> -
- - ); } diff --git a/ui/src/components/StatusPanels.jsx b/ui/src/components/StatusPanels.jsx index 30e6b7c..546eb2a 100644 --- a/ui/src/components/StatusPanels.jsx +++ b/ui/src/components/StatusPanels.jsx @@ -1,11 +1,16 @@ -import { KvList } from "./KvList"; - function formatNumber(value, digits = 3) { return Number(value ?? 0).toFixed(digits); } +function formatBackend(backend) { + if (backend === "ndi") return "NDI"; + if (backend === "decklink") return "DeckLink"; + if (backend === "none") return "None"; + return backend || "None"; +} + function formatEndpoint(endpoint) { - const backend = endpoint?.backend || "none"; + const backend = formatBackend(endpoint?.backend); const device = endpoint?.device ? ` ${endpoint.device}` : ""; return `${backend}${device}`; } @@ -16,9 +21,67 @@ function formatVideoMode(endpoint) { return `${resolution}${frameRate}`; } +function compactOutputStatus(videoOutput) { + const message = videoOutput?.statusMessage || ""; + if (!message) return videoOutput?.enabled ? "Running" : "Disabled"; + if (message.toLowerCase().includes("scheduled output running")) return "Running"; + if (message.toLowerCase().includes("disabled")) return "Disabled"; + return message; +} + +function StatusRows({ rows }) { + return ( +
+ {rows.map((row) => ( +
+
{row.label}
+
+ {row.value} + {row.meta && {row.meta}} +
+
+ ))} +
+ ); +} + export function StatusPanels({ app, performance, runtime, video, videoOutput }) { const budgetUsedPercent = Math.max(0, Math.min(100, Number(performance.budgetUsedPercent) || 0)); const outputEnabled = Boolean(videoOutput?.enabled); + const runtimeRows = [ + { + label: "Render", + value: `${formatNumber(performance.renderMs, 2)} ms`, + meta: `${formatNumber(performance.budgetUsedPercent, 1)}% of ${formatNumber(performance.frameBudgetMs, 2)} ms`, + }, + { + label: "Layers", + value: `${runtime.layerCount || 0}`, + meta: `Temporal cap ${app.maxTemporalHistoryFrames ?? 0}`, + }, + { + label: "Control", + value: `127.0.0.1:${app.serverPort}`, + meta: `Auto reload ${app.autoReload ? "on" : "off"}`, + }, + ]; + const videoRows = [ + { + label: "Input", + value: formatEndpoint(app.input), + meta: formatVideoMode(app.input), + }, + { + label: "Output", + value: formatEndpoint(app.output), + meta: `${formatVideoMode(app.output)} / ${video.width || 0} x ${video.height || 0}`, + }, + { + label: "Schedule", + value: `${videoOutput?.scheduleFailures ?? 0} failures`, + meta: compactOutputStatus(videoOutput), + }, + ]; return ( <> @@ -35,44 +98,20 @@ export function StatusPanels({ app, performance, runtime, video, videoOutput })
-
-
+
+

Runtime

- -
- Budget used -
-
+

Video

- +
diff --git a/ui/src/styles.css b/ui/src/styles.css index 74f4c3d..dab9c1b 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -292,6 +292,12 @@ pre { gap: 1.25rem; } +.telemetry-compact { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + .telemetry-section { min-width: 0; display: grid; @@ -299,6 +305,60 @@ pre { gap: 0.55rem; } +.telemetry-section--compact { + gap: 0.5rem; +} + +.status-rows { + display: grid; + gap: 0; + margin: 0; + padding: 0; + border-top: 1px solid var(--app-border); +} + +.status-row { + min-width: 0; + display: grid; + grid-template-columns: minmax(4.75rem, 0.38fr) minmax(0, 1fr); + gap: 0.75rem; + align-items: baseline; + padding: 0.52rem 0; + border-bottom: 1px solid rgba(48, 57, 71, 0.7); +} + +.status-row dt { + color: var(--app-muted); + font-size: 0.82rem; +} + +.status-row dd { + min-width: 0; + display: flex; + flex-wrap: wrap; + gap: 0.25rem 0.55rem; + align-items: baseline; + margin: 0; + overflow-wrap: anywhere; +} + +.status-row strong { + color: #f2f6fb; + font-size: 0.96rem; + font-weight: 730; + line-height: 1.25; +} + +.status-row span { + color: var(--app-muted); + font-size: 0.78rem; + line-height: 1.25; +} + +.status-meter { + padding-top: 0.1rem; +} + .mini-status { display: inline-flex; align-items: center; @@ -600,14 +660,19 @@ pre { font-size: 0.9rem; } -.config-combo-field { +.config-device-picker { + display: grid; + gap: 0.45rem; +} + +.config-device-picker__source { display: grid; grid-template-columns: minmax(0, 1fr) 38px; gap: 0.45rem; align-items: stretch; } -.config-combo-field .icon-button { +.config-device-picker__source .icon-button { width: 38px; min-width: 38px; height: 38px; @@ -1328,6 +1393,10 @@ pre { grid-template-columns: 1fr; } + .telemetry-compact { + grid-template-columns: 1fr; + } + .panel__header button, .stack-panel__reload, .layer-card__actions button {