Intial somewhat working version
This commit is contained in:
92
web/app.js
Normal file
92
web/app.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const state = {
|
||||
shaders: []
|
||||
};
|
||||
|
||||
const el = (id) => document.getElementById(id);
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const response = await fetch(path, {
|
||||
headers: { "content-type": "application/json" },
|
||||
...options
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(text || response.statusText);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function renderStatus(status) {
|
||||
el("device").textContent = status.deviceName || "No DeckLink device selected";
|
||||
el("running").textContent = status.running ? "Running" : "Stopped";
|
||||
el("mode").textContent = status.mode || "No signal";
|
||||
el("outputFormat").textContent = status.outputFormat || "Unavailable";
|
||||
el("frames").textContent = `${status.framesCaptured ?? 0} / ${status.framesOutput ?? 0}`;
|
||||
el("frameRate").textContent = Number(status.frameRate || 0).toFixed(2);
|
||||
el("dropped").textContent = status.framesDropped ?? 0;
|
||||
el("error").textContent = status.error || "";
|
||||
}
|
||||
|
||||
function renderShaders(payload) {
|
||||
state.shaders = payload.shaders || [];
|
||||
const host = el("shaders");
|
||||
host.innerHTML = "";
|
||||
for (const shader of state.shaders) {
|
||||
const card = document.createElement("div");
|
||||
card.className = "shader";
|
||||
const amount = shader.parameters.find((p) => p.id === "amount");
|
||||
card.innerHTML = `
|
||||
<header>
|
||||
<strong>${shader.name}</strong>
|
||||
<span>${shader.type}</span>
|
||||
</header>
|
||||
<label>
|
||||
<span>${amount.label}</span>
|
||||
<input type="range" min="${amount.min}" max="${amount.max}" step="0.01" value="${amount.value}">
|
||||
<output>${Number(amount.value).toFixed(2)}</output>
|
||||
</label>
|
||||
`;
|
||||
const input = card.querySelector("input");
|
||||
const output = card.querySelector("output");
|
||||
input.addEventListener("input", async () => {
|
||||
output.textContent = Number(input.value).toFixed(2);
|
||||
await api(`/api/shaders/${shader.id}/parameters`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ amount: Number(input.value) })
|
||||
});
|
||||
});
|
||||
host.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
renderStatus(await api("/api/status"));
|
||||
renderShaders(await api("/api/shaders"));
|
||||
}
|
||||
|
||||
function connectWs() {
|
||||
const ws = new WebSocket(`ws://${location.host}/ws`);
|
||||
ws.addEventListener("message", (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.type === "state") {
|
||||
renderStatus(message.status);
|
||||
renderShaders(message.shaders);
|
||||
}
|
||||
});
|
||||
ws.addEventListener("close", () => setTimeout(connectWs, 1000));
|
||||
}
|
||||
|
||||
el("start").addEventListener("click", async () => {
|
||||
try {
|
||||
renderStatus(await api("/api/pipeline/start", { method: "POST" }));
|
||||
} catch (error) {
|
||||
el("error").textContent = error.message;
|
||||
}
|
||||
});
|
||||
|
||||
el("stop").addEventListener("click", async () => {
|
||||
renderStatus(await api("/api/pipeline/stop", { method: "POST" }));
|
||||
});
|
||||
|
||||
refresh();
|
||||
connectWs();
|
||||
60
web/index.html
Normal file
60
web/index.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>video-shader</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<section class="topbar">
|
||||
<div>
|
||||
<h1>video-shader</h1>
|
||||
<p id="device">DeckLink device</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="start">Start</button>
|
||||
<button id="stop">Stop</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="status-grid">
|
||||
<article>
|
||||
<span>State</span>
|
||||
<strong id="running">Stopped</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Mode</span>
|
||||
<strong id="mode">No signal</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Output</span>
|
||||
<strong id="outputFormat">Unavailable</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Frames</span>
|
||||
<strong id="frames">0 / 0</strong>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="workbench">
|
||||
<div class="shader-list">
|
||||
<h2>Shader Stack</h2>
|
||||
<div id="shaders"></div>
|
||||
</div>
|
||||
<div class="meter">
|
||||
<h2>Runtime</h2>
|
||||
<dl>
|
||||
<dt>Frame rate</dt>
|
||||
<dd id="frameRate">0.00</dd>
|
||||
<dt>Dropped</dt>
|
||||
<dd id="dropped">0</dd>
|
||||
</dl>
|
||||
<p id="error"></p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
175
web/styles.css
Normal file
175
web/styles.css
Normal file
@@ -0,0 +1,175 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
font-family: "Segoe UI", Arial, sans-serif;
|
||||
background: #141717;
|
||||
color: #eef3ef;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(1120px, calc(100vw - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 28px 0;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 0 0 22px;
|
||||
border-bottom: 1px solid #33403b;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.topbar p,
|
||||
article span,
|
||||
dt {
|
||||
color: #aab8b0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
appearance: none;
|
||||
border: 1px solid #60736a;
|
||||
border-radius: 6px;
|
||||
background: #24302b;
|
||||
color: #f3fff7;
|
||||
min-width: 84px;
|
||||
height: 38px;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #314139;
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin: 22px 0;
|
||||
}
|
||||
|
||||
article,
|
||||
.shader-list,
|
||||
.meter {
|
||||
border: 1px solid #33403b;
|
||||
border-radius: 8px;
|
||||
background: #1b211f;
|
||||
}
|
||||
|
||||
article {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
article span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
article strong {
|
||||
display: block;
|
||||
min-height: 26px;
|
||||
font-size: 20px;
|
||||
font-weight: 650;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.workbench {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 340px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.shader-list,
|
||||
.meter {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.shader {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 14px;
|
||||
border: 1px solid #3c4b45;
|
||||
border-radius: 6px;
|
||||
background: #202925;
|
||||
}
|
||||
|
||||
.shader header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr 52px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #cdd8d1;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
dl {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 10px 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
#error {
|
||||
min-height: 24px;
|
||||
margin-top: 18px;
|
||||
color: #ffb4a8;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.topbar,
|
||||
.workbench {
|
||||
grid-template-columns: 1fr;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user