◷
@@ -311,12 +280,18 @@
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
+ // URL params:
+ // ?camera=Webcam override the source name to track for camera
+ // ?screen=Display override the source name to track for screen
+ // ?scene=Desktop LOCK the HUD to a specific scene (optional)
+ // When ?scene= is omitted (the default) the HUD follows whichever scene is
+ // currently program — so the same browser source can be referenced in both
+ // Desktop and Desktop (No Cam): switching scenes rebinds to the items in
+ // the now-active scene, and the camera/screen frames toggle/move with it.
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 SCENE_LOCK = params.get('scene') || null;
const camEl = document.getElementById('cam');
const scrEl = document.getElementById('scr');
@@ -336,10 +311,12 @@
};
}
- // 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.
+ // Frame binding for one tracked source. Holds mutable {sceneName, itemId}
+ // so the event filter reads through to the *current* binding — that lets
+ // rebind() swap scenes without leaking listeners or stale itemIds.
// Assumes alignment 5 (top-left) — OBS's default for newly added items.
- async function syncSourceFrame(obs, { sceneName, sourceName, frameEl, varPrefix }) {
+ function makeFrame(obs, sourceName, frameEl, varPrefix) {
+ const state = { sceneName: null, itemId: null };
const setVisible = (v) => frameEl.classList.toggle('off', !v);
const setTransform = (t) => {
if (!t) return;
@@ -350,30 +327,39 @@
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);
- }
- });
+ return {
+ async rebind(sceneName) {
+ state.sceneName = sceneName;
+ state.itemId = null;
+ if (!sceneName) { setVisible(false); return; }
+ try {
+ const r = await obs.call('GetSceneItemId',
+ { sceneName, sourceName });
+ state.itemId = r.sceneItemId;
+ const e = await obs.call('GetSceneItemEnabled',
+ { sceneName, sceneItemId: state.itemId });
+ setVisible(e.sceneItemEnabled);
+ const t = await obs.call('GetSceneItemTransform',
+ { sceneName, sceneItemId: state.itemId });
+ setTransform(t.sceneItemTransform);
+ } catch {
+ // Source isn't in this scene (or scene doesn't exist) — hide
+ // the frame outright. This is what makes "Desktop (No Cam)"
+ // work cleanly even if Camera were fully removed (rather than
+ // just disabled).
+ setVisible(false);
+ }
+ },
+ handleEvent(d) {
+ if (d.eventData?.sceneName !== state.sceneName
+ || d.eventData?.sceneItemId !== state.itemId) return;
+ if (d.eventType === 'SceneItemEnableStateChanged') {
+ setVisible(d.eventData.sceneItemEnabled);
+ } else if (d.eventType === 'SceneItemTransformChanged') {
+ setTransform(d.eventData.sceneItemTransform);
+ }
+ },
+ };
}
async function connectOBS() {
@@ -381,14 +367,34 @@
try {
const obs = new OBSWSMini(window.__OBSWS.url, window.__OBSWS.password);
await obs.connect();
- obs.addEventListener('close', () => {
- setTimeout(connectOBS, 2500);
+ obs.addEventListener('close', () => setTimeout(connectOBS, 2500));
+
+ const camFrame = makeFrame(obs, CAM_SOURCE, camEl, 'cam');
+ const scrFrame = makeFrame(obs, SCR_SOURCE, scrEl, 'scr');
+
+ // Resolve the scene to track. With ?scene= explicit, lock to it;
+ // otherwise follow whatever's currently program. Modern obs-websocket
+ // returns currentProgramSceneName; older builds returned sceneName —
+ // accept both.
+ let scene = SCENE_LOCK;
+ if (!scene) {
+ try {
+ const r = await obs.call('GetCurrentProgramScene');
+ scene = r.currentProgramSceneName || r.sceneName || null;
+ } catch { /* leave null; rebind on first scene change */ }
+ }
+ await Promise.all([camFrame.rebind(scene), scrFrame.rebind(scene)]);
+
+ obs.addEventListener('event', async (ev) => {
+ const d = ev.detail || {};
+ if (!SCENE_LOCK && d.eventType === 'CurrentProgramSceneChanged') {
+ const next = d.eventData?.sceneName;
+ await Promise.all([camFrame.rebind(next), scrFrame.rebind(next)]);
+ return;
+ }
+ camFrame.handleEvent(d);
+ scrFrame.handleEvent(d);
});
- // 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);
diff --git a/music-box/cover.html b/music-box/cover.html
new file mode 100644
index 0000000..621f6c2
--- /dev/null
+++ b/music-box/cover.html
@@ -0,0 +1,295 @@
+
+
+
+
+
Music Box cover
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+ — NO ART —
+ awaiting cover…
+
+
+
+
+
+ ▶connecting…
+
+
+
+
+
+
+
diff --git a/music-box/nc-music-box.html b/music-box/nc-music-box.html
new file mode 100644
index 0000000..8b2b2ae
--- /dev/null
+++ b/music-box/nc-music-box.html
@@ -0,0 +1,804 @@
+
+
+
+
+
OPHI-118 / NC-MUSIC-BOX
+
+
+
+
+
+
+
+
+
+
+ ┤ TERMINAL ├
+
+
+
+
+ ↳next:— end of queue —
+
+
+ ↳then:—
+
+
+ ↳then:—
+
+
+
+ ▶connecting to daemon…[--:-- / --:--]
+
+
+
+
+
+
+
+
+
+
+ CHAT
+
+ !skipnext track
+ !queuepeek next 3
+ !infofull metadata
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugin_manager/modules.json b/plugin_manager/modules.json
index 00d1058..c7999f0 100644
--- a/plugin_manager/modules.json
+++ b/plugin_manager/modules.json
@@ -9,5 +9,18 @@
"services": [],
"sources": [],
"version": ""
+ },
+ {
+ "display_name": "phandasm_waveform",
+ "enabled": true,
+ "encoders": [],
+ "id": "",
+ "module_name": "waveform",
+ "outputs": [],
+ "services": [],
+ "sources": [
+ "phandasm_waveform_source"
+ ],
+ "version": ""
}
]
diff --git a/user.ini b/user.ini
index 094beea..8ae5861 100644
--- a/user.ini
+++ b/user.ini
@@ -25,8 +25,8 @@ cx=720
cy=580
[BasicWindow]
-geometry=AdnQywADAAAAAAAAAAAAAAAACf8AAAVDAAAKAQAAABkAAA3AAAAEHAAAAAECAAAACgAAAAAAAAAAAAAACf8AAAVD
-DockState=AAAA/wAAAAD9AAAAAQAAAAMAAAoAAAABKPwBAAAABvsAAAAUAHMAYwBlAG4AZQBzAEQAbwBjAGsBAAAAAAAAAdgAAACYAP////sAAAAWAHMAbwB1AHIAYwBlAHMARABvAGMAawEAAAHcAAAB1QAAAJgA////+wAAABIAbQBpAHgAZQByAEQAbwBjAGsBAAADtQAAAroAAAECAP////sAAAAeAHQAcgBhAG4AcwBpAHQAaQBvAG4AcwBEAG8AYwBrAQAABnMAAAHSAAAArQD////7AAAAGABjAG8AbgB0AHIAbwBsAHMARABvAGMAawEAAAhJAAABtwAAAKIA////+wAAABIAcwB0AGEAdABzAEQAbwBjAGsCAAAExAAAAXAAAAK8AAAAyAAACgAAAAPcAAAABAAAAAQAAAAIAAAACPwAAAAA
+geometry=AdnQywADAAAAAAAAAAAAAAAACf8AAAVDAAAGPwAAABkAAAn+AAAEFgAAAAECAAAACgAAAAAAAAAAAAAACf8AAAVD
+DockState=AAAA/wAAAAD9AAAAAQAAAAMAAAoAAAABA/wBAAAABvsAAAAUAHMAYwBlAG4AZQBzAEQAbwBjAGsBAAAAAAAAAdgAAACYAP////sAAAAWAHMAbwB1AHIAYwBlAHMARABvAGMAawEAAAHcAAAB1QAAAJgA////+wAAABIAbQBpAHgAZQByAEQAbwBjAGsBAAADtQAAAroAAAECAP////sAAAAeAHQAcgBhAG4AcwBpAHQAaQBvAG4AcwBEAG8AYwBrAQAABnMAAAHSAAAArQD////7AAAAGABjAG8AbgB0AHIAbwBsAHMARABvAGMAawEAAAhJAAABtwAAAKIA////+wAAABIAcwB0AGEAdABzAEQAbwBjAGsCAAAExAAAAXAAAAK8AAAAyAAACgAAAAQBAAAABAAAAAQAAAAIAAAACPwAAAAA
PreviewEnabled=true
AlwaysOnTop=false
SceneDuplicationMode=true