Replaces the per-scene HTML directories (landing/, loading/, game/,
desktop/, goodbye/, music-box/, music/) with a single Laravel app
serving every overlay over HTTP. Supervisord runs `php artisan serve`
on 127.0.0.1:1118 and the OBS scene JSON now references HTTP routes
instead of file:// URLs.
Highlights:
- public/css/hud.css consolidates the duplicated HUD chrome,
scanlines/vignette/flicker, terminal styling, and pulse keyframes
that were copy-pasted across all seven scenes.
- Blade partials own hud-strip, crt-overlays, obs-ws-scripts,
camera-frame, screen-frame; the seven scenes extend a shared
overlay layout.
- Artisan commands (`rig:setup`, `rig:telemetry`, `rig:loading`,
`rig:playlist`, `rig:cmd`) replace the shell scripts that wrote
per-rig JSON snapshots. TwitchHelix + HardwareSnapshot services
handle the work the bash + Python helpers used to.
- ObsWsClient + MusicCommandController kill the 250 ms cmd.js poll
in the music daemon: POST /cmd/{skip|prev|pause|resume} opens a
short-lived Pawl WS, authenticates, and broadcasts mpd:cmd.
- AudioController streams files from the configured music dirs so
CEF can load tracks under the HTTP origin (Chromium blocks
HTTP-origin pages from loading file:// media).
- DataController serves /data/playlist.js (with ETag mtime cache)
and /cover.jpg (no-store) so the existing overlays' window.__PLAYLIST
and cover.jpg cache-bust pattern keeps working.
scripts/obs-webapp.supervisord.conf is the supervisord unit; install
to /etc/supervisor.d/obs-webapp.conf.
scripts/rewrite-scene-urls.py is a one-shot tool that rewrites
basic/scenes/Default_Stream_HUD.json from file:// to HTTP URLs.
basic/scenes/Default_Stream_HUD.json.pre-webapp is the rollback
artifact (made with OBS closed; full pre-migration state).
The seven old scene directories, vendor/, and the bash scripts are
still on disk pending visual verification; the next commit will
prune them.
164 lines
5.1 KiB
PHP
164 lines
5.1 KiB
PHP
@extends('layouts.overlay')
|
||
|
||
@section('title', 'STAND BY')
|
||
|
||
@section('styles')
|
||
<style>
|
||
body.overlay-body {
|
||
display: grid;
|
||
grid-template-rows: auto 1fr auto auto;
|
||
gap: 24px;
|
||
}
|
||
.stage {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 36px;
|
||
}
|
||
.mark {
|
||
width: 360px; height: 360px;
|
||
filter: drop-shadow(0 0 22px rgba(58, 209, 255, 0.35));
|
||
}
|
||
.bars {
|
||
display: flex;
|
||
width: 720px;
|
||
height: 14px;
|
||
opacity: 0.32;
|
||
}
|
||
.bars i { flex: 1; display: block; }
|
||
.bars i:nth-child(1) { background: #c7c7c7; }
|
||
.bars i:nth-child(2) { background: #c7c700; }
|
||
.bars i:nth-child(3) { background: #00c7c7; }
|
||
.bars i:nth-child(4) { background: #00c700; }
|
||
.bars i:nth-child(5) { background: #c700c7; }
|
||
.bars i:nth-child(6) { background: #c70000; }
|
||
.bars i:nth-child(7) { background: #0000c7; }
|
||
</style>
|
||
@endsection
|
||
|
||
@push('data-scripts')
|
||
<script>window.__TEL = @json($telemetry);</script>
|
||
@endpush
|
||
|
||
@section('content')
|
||
@include('partials.hud-strip', [
|
||
'variant' => 'live',
|
||
'label' => 'TRANSMISSION',
|
||
'extraIdSlot' => '<span class="station"><span class="station-id">OPHI-118</span><span class="sep">//</span><span class="rig" id="rigName">--</span></span>',
|
||
])
|
||
|
||
<main class="stage">
|
||
<svg class="mark" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||
<circle cx="100" cy="100" r="92" fill="none" stroke="#3ad1ff" stroke-width="1.2" opacity="0.35"/>
|
||
<circle cx="100" cy="100" r="80" fill="none" stroke="#3ad1ff" stroke-width="2.5"/>
|
||
<g stroke="#3ad1ff" stroke-width="2" opacity="0.55">
|
||
<line x1="100" y1="8" x2="100" y2="22"/>
|
||
<line x1="100" y1="178" x2="100" y2="192"/>
|
||
<line x1="8" y1="100" x2="22" y2="100"/>
|
||
<line x1="178" y1="100" x2="192" y2="100"/>
|
||
</g>
|
||
<polygon points="100,52 148,140 52,140"
|
||
fill="none" stroke="#e63a2e" stroke-width="3.5" stroke-linejoin="miter"/>
|
||
<circle cx="100" cy="116" r="4" fill="#ffd000"/>
|
||
</svg>
|
||
<div class="bars"><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div>
|
||
</main>
|
||
|
||
<section class="standby">
|
||
<div class="big">PLEASE STAND BY</div>
|
||
<div class="sub">— DO NOT ADJUST YOUR RECEIVER —</div>
|
||
</section>
|
||
|
||
<footer class="foot">
|
||
<span>CH 118.0 MHz</span>
|
||
<span id="rig">— TELEMETRY —</span>
|
||
<span class="warn">▲ AUDIO ACTIVE</span>
|
||
</footer>
|
||
@endsection
|
||
|
||
@section('scripts')
|
||
<script>
|
||
'use strict';
|
||
const rigNameEl = document.getElementById('rigName');
|
||
const RIG_NAME = (window.__TEL && window.__TEL.rig) || 'UNKNOWN';
|
||
const upperRig = String(RIG_NAME).toUpperCase();
|
||
const scrambleRig = ({ initial = false } = {}) => {
|
||
if (!upperRig.length) { rigNameEl.textContent = '--'; return; }
|
||
HUD.scrambleReveal(rigNameEl, upperRig, {
|
||
scrambleMs: initial ? 600 : 360, stepMs: 50, resolveStepMs: 60,
|
||
});
|
||
};
|
||
setTimeout(() => scrambleRig({ initial: true }), 600);
|
||
setInterval(scrambleRig, 9000);
|
||
|
||
// Rotating telemetry footer.
|
||
const rigEl = document.getElementById('rig');
|
||
const tel = window.__TEL || null;
|
||
const shortCpu = (m) => (m || '')
|
||
.replace(/\(R\)|\(TM\)/g, '')
|
||
.replace(/\s+\S+-Core\s+Processor\b/i, '')
|
||
.replace(/\s+Processor\b/i, '')
|
||
.replace(/\s+CPU\b.*$/i, '')
|
||
.replace(/\s+@.*$/, '')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
const lines = [
|
||
() => {
|
||
if (!tel?.cpu?.model) return null;
|
||
let s = `CPU · ${shortCpu(tel.cpu.model)}`;
|
||
if (tel.cpu.threads) s += ` · ${tel.cpu.threads}T`;
|
||
if (tel.mem?.totalGB) s += ` · MEM ${tel.mem.totalGB} GB`;
|
||
return s;
|
||
},
|
||
() => {
|
||
if (!tel?.gpu?.name) return null;
|
||
let s = `GPU · ${tel.gpu.name}`;
|
||
if (tel.gpu.vramTotalMB) s += ` · ${(tel.gpu.vramTotalMB / 1024).toFixed(0)} GB VRAM`;
|
||
return s;
|
||
},
|
||
() => {
|
||
const o = tel?.obs;
|
||
if (!o?.output?.w) return null;
|
||
let s = `OUTPUT · ${o.output.w}×${o.output.h}`;
|
||
if (o.fps) s += ` @ ${o.fps} FPS`;
|
||
if (o.stream?.encoder) s += ` · ${o.stream.encoder}`;
|
||
return s;
|
||
},
|
||
() => {
|
||
const s = tel?.obs?.stream;
|
||
if (!s) return null;
|
||
const parts = [];
|
||
if (s.rateControl) parts.push(s.rateControl);
|
||
if (s.bitrateKbps) parts.push(`${s.bitrateKbps} kbps`);
|
||
if (s.profile) parts.push(s.profile.toUpperCase());
|
||
return parts.length ? `STREAM · ${parts.join(' · ')}` : null;
|
||
},
|
||
() => tel?.host?.kernel ? `KERNEL · ${tel.host.kernel}` : null,
|
||
];
|
||
let lineIdx = 0;
|
||
const nextLine = () => {
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const v = lines[lineIdx]();
|
||
if (v) return v;
|
||
lineIdx = (lineIdx + 1) % lines.length;
|
||
}
|
||
return '— TELEMETRY OFFLINE —';
|
||
};
|
||
rigEl.textContent = nextLine();
|
||
const swapLine = () => {
|
||
const text = nextLine();
|
||
const len = Math.max(text.length, 1);
|
||
HUD.scrambleReveal(rigEl, text, {
|
||
scrambleMs: 280,
|
||
stepMs: 45,
|
||
resolveStepMs: Math.max(15, Math.floor(700 / len)),
|
||
});
|
||
};
|
||
setInterval(() => {
|
||
lineIdx = (lineIdx + 1) % lines.length;
|
||
swapLine();
|
||
}, 6000);
|
||
</script>
|
||
@endsection
|