webapp: introduce Laravel app under webapp/ for scene overlays
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.
This commit is contained in:
163
webapp/resources/views/overlays/landing.blade.php
Normal file
163
webapp/resources/views/overlays/landing.blade.php
Normal file
@@ -0,0 +1,163 @@
|
||||
@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
|
||||
Reference in New Issue
Block a user