Files
obs-config/desktop/index.html
2026-04-27 14:59:24 +02:00

401 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OPHI-118 / DESKTOP</title>
<style>
:root {
--ink: #e8e8e0;
--ink-dim: rgba(232, 232, 224, 0.62);
--hud: #4fd2ff;
--hud-dim: #2a7fa6;
--hud-edge: rgba(79, 210, 255, 0.85);
--hud-glow: rgba(79, 210, 255, 0.40);
--accent: #e63a2e;
--warn: #ffd000;
--term-fg: #5fdc62;
--term-fg-dim:#2c8d2f;
--mono: 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', monospace;
--display: 'DejaVu Sans', 'Liberation Sans', 'Helvetica', 'Arial', sans-serif;
--pad-y: 40px;
--pad-x: 72px;
/* Camera frame transform — auto-synced from the Desktop scene's Camera
item via OBS WS. These defaults are first-paint fallback. */
--cam-x: 1937px;
--cam-y: 98px;
--cam-w: 549px;
--cam-h: 309px;
--cam-pad: 10px;
/* Screen-capture frame transform — auto-synced from the Screen Capture
item the same way. Defaults match the current scene's bounds. */
--scr-x: 10px;
--scr-y: 14px;
--scr-w: 1976px;
--scr-h: 1209px;
--scr-pad: 0px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%;
background: transparent;
color: var(--ink);
font-family: var(--display);
overflow: hidden;
user-select: none;
}
/* ───────── Top HUD ───────── */
.hud {
position: absolute;
top: var(--pad-y);
left: var(--pad-x);
right: var(--pad-x);
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
font-family: var(--mono);
font-size: 20px;
font-weight: 700;
color: var(--hud);
letter-spacing: 0.08em;
text-shadow: 0 0 8px rgba(7, 8, 13, 0.9), 0 0 4px rgba(7, 8, 13, 0.9);
}
.hud .group { display: flex; gap: 24px; align-items: center; }
.hud > .group:nth-child(1) { justify-self: start; }
.hud > .group:nth-child(2) { justify-self: center; }
.hud > .group:nth-child(3) { justify-self: end; }
.hud .dim { color: var(--hud-dim); }
.hud .group > span { display: inline-flex; align-items: center; gap: 10px; }
.hud .led {
display: inline-block;
width: 10px; height: 10px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 10px var(--accent);
margin-top: -0.08em;
animation: rec-blink 1.6s ease-in-out infinite;
}
#signal {
display: inline-block;
transform: translateY(-0.10em);
}
@keyframes rec-blink {
0%, 55% { opacity: 1; }
65%, 100% { opacity: 0.2; }
}
/* ───────── Camera frame (shared logic with game/index.html) ───────── */
.camera-frame {
position: absolute;
top: calc(var(--cam-y) - var(--cam-pad));
left: calc(var(--cam-x) - var(--cam-pad));
width: calc(var(--cam-w) + var(--cam-pad) * 2);
height: calc(var(--cam-h) + var(--cam-pad) * 2);
pointer-events: none;
background: rgba(6, 10, 18, 0.85);
border: 1.5px solid rgba(79, 210, 255, 0.55);
box-shadow:
0 0 28px rgba(79, 210, 255, 0.20),
inset 0 0 60px rgba(0, 0, 0, 0.55);
}
/* ───────── Screen-capture frame ─────────
Same pattern as the camera frame — corner ticks + label + tracked size —
but transparent (the desktop renders behind, must show through) and a
thinner perimeter line, since at full-canvas scale a bright cyan rectangle
all around the desktop reads as too loud. Bright ticks carry the "this is
a framed feed" signal; the line itself is just a hint. */
.screen-frame {
position: absolute;
top: calc(var(--scr-y) - var(--scr-pad));
left: calc(var(--scr-x) - var(--scr-pad));
width: calc(var(--scr-w) + var(--scr-pad) * 2);
height: calc(var(--scr-h) + var(--scr-pad) * 2);
pointer-events: none;
background: transparent;
border: 1px solid rgba(79, 210, 255, 0.30);
box-shadow: 0 0 18px rgba(79, 210, 255, 0.08);
}
.camera-frame .tick,
.screen-frame .tick {
position: absolute;
width: 24px; height: 24px;
border: 3px solid var(--hud-edge);
box-shadow: 0 0 8px var(--hud-glow);
}
.camera-frame .tick.tl, .screen-frame .tick.tl { top: -2px; left: -2px; border-right: none; border-bottom: none; }
.camera-frame .tick.tr, .screen-frame .tick.tr { top: -2px; right: -2px; border-left: none; border-bottom: none; }
.camera-frame .tick.bl, .screen-frame .tick.bl { bottom: -2px; left: -2px; border-right: none; border-top: none; }
.camera-frame .tick.br, .screen-frame .tick.br { bottom: -2px; right: -2px; border-left: none; border-top: none; }
.camera-frame .label,
.screen-frame .label {
position: absolute;
top: 10px; left: 14px;
font-family: var(--mono);
font-size: 12px;
color: var(--hud);
letter-spacing: 0.32em;
text-shadow: 0 0 4px rgba(7, 8, 13, 0.9);
}
.camera-frame .placeholder {
position: absolute; inset: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 14px;
font-family: var(--mono);
color: var(--hud);
pointer-events: none;
}
.camera-frame .placeholder svg { opacity: 0.6; }
.camera-frame .placeholder .ph-text {
font-size: 13px;
letter-spacing: 0.45em;
color: rgba(79, 210, 255, 0.7);
}
.camera-frame .placeholder .ph-coords {
font-size: 11px;
letter-spacing: 0.20em;
color: var(--hud-dim);
}
.camera-frame.off,
.screen-frame.off { display: none; }
/* ───────── Bottom status bar ───────── */
.status-bar {
position: absolute;
bottom: 0;
left: 0; right: 0;
padding: 22px var(--pad-x) 26px;
background:
linear-gradient(
to top,
rgba(7, 8, 13, 0.96) 0%,
rgba(7, 8, 13, 0.96) 65%,
rgba(7, 8, 13, 0.50) 90%,
rgba(7, 8, 13, 0) 100%
);
border-top: 1px solid rgba(79, 210, 255, 0.28);
box-shadow: 0 -2px 16px rgba(0, 0, 0, 0.5);
font-family: var(--mono);
letter-spacing: 0.08em;
}
.status-line { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.status-line + .status-line { margin-top: 8px; }
.status-line .sep { color: var(--hud-dim); }
.live-block {
display: flex; align-items: center; gap: 10px;
color: var(--accent);
font-weight: 700;
letter-spacing: 0.14em;
font-size: 18px;
}
.live-dot {
width: 11px; height: 11px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 10px var(--accent);
animation: rec-blink 1.6s ease-in-out infinite;
}
/* The "DESKTOP" label fills the slot the Game HUD uses for game name. */
.scene-name {
color: var(--ink);
font-weight: 700;
font-size: 22px;
letter-spacing: 0.10em;
}
.meta-line {
color: var(--ink-dim);
font-size: 16px;
}
.meta-line .icon { color: var(--hud); }
</style>
</head>
<body>
<header class="hud">
<div class="group">
<span><span class="led"></span>TRANSMISSION</span>
<span>OPHI-118 // DESKTOP</span>
</div>
<div class="group">
<span class="dim">SIGNAL</span>
<span id="signal">▮▮▮▯▯</span>
</div>
<div class="group">
<span class="dim">UTC</span>
<span id="clock">--:--:--</span>
</div>
</header>
<div class="screen-frame" id="scr">
<span class="tick tl"></span>
<span class="tick tr"></span>
<span class="tick bl"></span>
<span class="tick br"></span>
<span class="label">SCR 01</span>
</div>
<div class="camera-frame" id="cam">
<span class="tick tl"></span>
<span class="tick tr"></span>
<span class="tick bl"></span>
<span class="tick br"></span>
<span class="label">CAM 01</span>
<div class="placeholder">
<svg width="64" height="48" viewBox="0 0 64 48" xmlns="http://www.w3.org/2000/svg" fill="none">
<rect x="2" y="8" width="44" height="32" rx="2" stroke="#4fd2ff" stroke-width="2"/>
<path d="M46 18 L60 10 L60 38 L46 30 Z" stroke="#4fd2ff" stroke-width="2" stroke-linejoin="round"/>
<circle cx="14" cy="14" r="2" fill="#e63a2e"/>
</svg>
<span class="ph-text">— WAITING FOR CAMERA —</span>
<span class="ph-coords">auto-syncs from OBS scene transform</span>
</div>
</div>
<footer class="status-bar">
<div class="status-line">
<div class="live-block">
<span class="live-dot"></span>
<span>LIVE</span>
</div>
<span class="sep">·</span>
<span class="scene-name">DESKTOP</span>
</div>
<div class="status-line meta-line">
<span class="icon"></span>
<span class="elapsed" id="elapsed">00:00:00</span>
<span class="sep">·</span>
<span class="icon" title="microphone">🎙</span>
<span style="color: var(--term-fg);">ON</span>
</div>
</footer>
<script src="../vendor/obs-config.js"></script>
<script src="../vendor/obs-ws-mini.js"></script>
<script>
'use strict';
const pad = n => String(n).padStart(2, '0');
// ── UTC clock ──
const clockEl = document.getElementById('clock');
function tickClock() {
const d = new Date();
clockEl.textContent =
`${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
}
tickClock(); setInterval(tickClock, 1000);
// ── Signal bar fluctuation (steady, like Game) ──
const sigEl = document.getElementById('signal');
setInterval(() => {
const r = Math.random();
const n = r < 0.06 ? 2 : r < 0.70 ? 3 : r < 0.95 ? 4 : 5;
sigEl.textContent = '▮'.repeat(n) + '▯'.repeat(5 - n);
}, 1500);
// ── Stream elapsed time (since page load / scene activation) ──
const streamStart = Date.now();
function tickElapsed() {
const s = Math.floor((Date.now() - streamStart) / 1000);
const h = Math.floor(s / 3600);
const mm = Math.floor((s / 60) % 60);
const ss = s % 60;
document.getElementById('elapsed').textContent = `${pad(h)}:${pad(mm)}:${pad(ss)}`;
}
tickElapsed(); setInterval(tickElapsed, 1000);
// ── OBS WS: live frame sync for camera + screen capture ──
// URL params let you rename the sources / scene without editing JS:
// ?camera=Webcam&screen=Display&scene=Desktop
const params = new URLSearchParams(location.search);
const CAM_SOURCE = params.get('camera') || 'Camera';
const SCR_SOURCE = params.get('screen') || 'Screen Capture (PipeWire)';
const SCENE = params.get('scene') || 'Desktop';
const camEl = document.getElementById('cam');
const scrEl = document.getElementById('scr');
const rootStyle = document.documentElement.style;
// Compute rendered (w, h) on canvas from an OBS sceneItemTransform.
// Camera item has no bounds → scale × source. Screen capture uses
// bounds_type=stretch (1) → boundsWidth/boundsHeight are authoritative.
function renderedSize(t) {
const useBounds = t.boundsType
&& t.boundsType !== 'OBS_BOUNDS_NONE'
&& (t.boundsWidth ?? 0) > 0;
if (useBounds) return { w: t.boundsWidth, h: t.boundsHeight };
return {
w: (t.sourceWidth ?? 0) * (t.scaleX ?? 1),
h: (t.sourceHeight ?? 0) * (t.scaleY ?? 1),
};
}
// Bind a frame element to a scene item: live-sync size/position into the
// --{prefix}-x/y/w/h CSS vars and toggle .off on enable changes.
// Assumes alignment 5 (top-left) — OBS's default for newly added items.
async function syncSourceFrame(obs, { sceneName, sourceName, frameEl, varPrefix }) {
const setVisible = (v) => frameEl.classList.toggle('off', !v);
const setTransform = (t) => {
if (!t) return;
const { w, h } = renderedSize(t);
if (!(w > 0 && h > 0)) return;
rootStyle.setProperty(`--${varPrefix}-x`, `${t.positionX}px`);
rootStyle.setProperty(`--${varPrefix}-y`, `${t.positionY}px`);
rootStyle.setProperty(`--${varPrefix}-w`, `${w}px`);
rootStyle.setProperty(`--${varPrefix}-h`, `${h}px`);
};
let itemId = null;
try {
const r = await obs.call('GetSceneItemId', { sceneName, sourceName });
itemId = r.sceneItemId;
const e = await obs.call('GetSceneItemEnabled',
{ sceneName, sceneItemId: itemId });
setVisible(e.sceneItemEnabled);
const t = await obs.call('GetSceneItemTransform',
{ sceneName, sceneItemId: itemId });
setTransform(t.sceneItemTransform);
} catch (err) {
console.warn(`[desktop] frame sync failed (${sceneName}/${sourceName}):`, err.message);
return;
}
obs.addEventListener('event', (ev) => {
const d = ev.detail || {};
if (d.eventData?.sceneName !== sceneName
|| d.eventData?.sceneItemId !== itemId) return;
if (d.eventType === 'SceneItemEnableStateChanged') {
setVisible(d.eventData.sceneItemEnabled);
} else if (d.eventType === 'SceneItemTransformChanged') {
setTransform(d.eventData.sceneItemTransform);
}
});
}
async function connectOBS() {
if (!window.__OBSWS || !window.OBSWSMini) return;
try {
const obs = new OBSWSMini(window.__OBSWS.url, window.__OBSWS.password);
await obs.connect();
obs.addEventListener('close', () => {
setTimeout(connectOBS, 2500);
});
// Run both syncs in parallel — independent items, independent failures.
await Promise.all([
syncSourceFrame(obs, { sceneName: SCENE, sourceName: CAM_SOURCE, frameEl: camEl, varPrefix: 'cam' }),
syncSourceFrame(obs, { sceneName: SCENE, sourceName: SCR_SOURCE, frameEl: scrEl, varPrefix: 'scr' }),
]);
} catch (e) {
// OBS WS optional — silently retry; defaults stand meanwhile.
setTimeout(connectOBS, 2500);
}
}
connectOBS();
</script>
</body>
</html>